├── .flake8 ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── ci.yml │ ├── post-dependabot.yml │ └── shiftleft-analysis.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.rst ├── demos ├── __init__.py ├── blog │ ├── README.rst │ ├── aiohttpdemo_blog │ │ ├── __init__.py │ │ ├── db.py │ │ ├── db_auth.py │ │ ├── forms.py │ │ ├── main.py │ │ ├── routes.py │ │ ├── security.py │ │ ├── settings.py │ │ ├── templates │ │ │ ├── base.html │ │ │ ├── create_post.html │ │ │ ├── index.html │ │ │ └── login.html │ │ ├── typedefs.py │ │ └── views.py │ ├── config │ │ ├── admin_config.toml │ │ ├── test_config.toml │ │ └── user_config.toml │ ├── db_helpers.py │ ├── pytest.ini │ ├── requirements.txt │ ├── setup.py │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_stuff.py ├── chat │ ├── README.rst │ ├── aiohttpdemo_chat │ │ ├── __init__.py │ │ ├── main.py │ │ ├── templates │ │ │ └── index.html │ │ └── views.py │ ├── pytest.ini │ ├── requirements.txt │ ├── setup.py │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_chat.py ├── graphql-demo │ ├── Makefile │ ├── README.md │ ├── __init__.py │ ├── docker-compose.yaml │ ├── graph │ │ ├── .flake8 │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── contrib.py │ │ │ ├── dataloaders.py │ │ │ ├── models │ │ │ │ ├── __init__.py │ │ │ │ ├── room.py │ │ │ │ └── user.py │ │ │ ├── mutations │ │ │ │ ├── __init__.py │ │ │ │ └── messages.py │ │ │ ├── queries │ │ │ │ ├── __init__.py │ │ │ │ ├── rooms.py │ │ │ │ └── user.py │ │ │ ├── subscriptions │ │ │ │ ├── __init__.py │ │ │ │ └── messages.py │ │ │ ├── tests │ │ │ │ ├── test_messages.py │ │ │ │ ├── test_messages_mutations.py │ │ │ │ ├── test_rooms.py │ │ │ │ └── test_viewer.py │ │ │ └── views.py │ │ ├── app.py │ │ ├── auth │ │ │ ├── __init__.py │ │ │ ├── db_utils.py │ │ │ ├── enums.py │ │ │ ├── models.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ └── test_auth_db_utils.py │ │ ├── chat │ │ │ ├── __init__.py │ │ │ ├── db_utils.py │ │ │ ├── models.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ └── test_chat_db_utils.py │ │ ├── config │ │ │ ├── api.test.yml │ │ │ └── api.yml │ │ ├── conftest.py │ │ ├── constants.py │ │ ├── db.py │ │ ├── main │ │ │ ├── __init__.py │ │ │ └── views.py │ │ ├── routes.py │ │ ├── static │ │ │ └── bundle.js │ │ ├── templates │ │ │ └── index.jinja2 │ │ └── utils.py │ ├── mypy.ini │ ├── prepare_database.py │ ├── pytest.ini │ ├── requirements-dev.txt │ ├── setup.py │ └── ui │ │ ├── .babelrc │ │ ├── package.json │ │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── client │ │ │ ├── Provider.js │ │ │ └── index.js │ │ ├── components │ │ │ ├── ChatMessages │ │ │ │ ├── ChatMessages.css │ │ │ │ └── ChatMessages.js │ │ │ ├── Header │ │ │ │ ├── Header.css │ │ │ │ └── Header.js │ │ │ ├── MessageBox │ │ │ │ ├── MessageBox.css │ │ │ │ └── MessageBox.js │ │ │ ├── Row │ │ │ │ ├── Row.css │ │ │ │ └── Row.js │ │ │ ├── SendMessageButton │ │ │ │ ├── SendMessageButton.css │ │ │ │ └── SendMessageButton.js │ │ │ └── index.js │ │ ├── index.css │ │ ├── index.js │ │ └── utils.js │ │ ├── webpack.config.babel.js │ │ └── yarn.lock ├── imagetagger │ ├── Makefile │ ├── README.rst │ ├── config │ │ ├── api.dev.yml │ │ ├── api.prod.yml │ │ └── api.test.yml │ ├── imagetagger │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── app.py │ │ ├── constants.py │ │ ├── routes.py │ │ ├── static │ │ │ ├── images │ │ │ │ ├── favicon.ico │ │ │ │ └── logo.png │ │ │ └── styles │ │ │ │ └── main.css │ │ ├── templates │ │ │ └── index.html │ │ ├── types.py │ │ ├── utils.py │ │ ├── views.py │ │ └── worker.py │ ├── pytest.ini │ ├── requirements.txt │ ├── setup.py │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── data │ │ ├── aircraft.jpg │ │ ├── hotdog.jpg │ │ └── mobilenet.h5 │ │ └── test_views.py ├── moderator │ ├── .babelrc │ ├── .editorconfig │ ├── .eslintrc │ ├── .postcssrc │ ├── .snyk │ ├── Makefile │ ├── README.rst │ ├── config │ │ └── config.yml │ ├── docs │ │ └── preview.png │ ├── model │ │ └── pipeline.dat │ ├── moderator │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── build_model.py │ │ ├── consts.py │ │ ├── exceptions.py │ │ ├── handlers.py │ │ ├── main.py │ │ ├── model │ │ │ ├── __init__.py │ │ │ └── pipeline.py │ │ ├── routes.py │ │ ├── utils.py │ │ └── worker.py │ ├── package.json │ ├── postcss.config.js │ ├── pytest.ini │ ├── requirements-dev.txt │ ├── setup.py │ ├── static │ │ ├── .gitkeep │ │ ├── App.083d307f.js │ │ ├── App.c3ef49a7.map │ │ ├── App.d8c8d9ba.css │ │ └── index.html │ ├── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_basic.py │ │ ├── test_pipeline.py │ │ └── toxic.csv │ ├── ui │ │ ├── App │ │ │ ├── App.scss │ │ │ ├── Results │ │ │ │ ├── Results.scss │ │ │ │ └── index.js │ │ │ └── index.js │ │ └── index.html │ └── yarn.lock ├── moderator_bot │ ├── .editorconfig │ ├── .gitattributes │ ├── .gitignore │ ├── README.md │ ├── configs │ │ └── base.yml │ ├── model │ │ └── pipeline.dat │ ├── moderator_bot │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── app.py │ │ ├── giphy.py │ │ ├── handlers.py │ │ ├── router.py │ │ ├── server.py │ │ ├── settings.py │ │ ├── utils.py │ │ └── worker.py │ ├── pytest.ini │ ├── requirements-dev.txt │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_handlers.py ├── motortwit │ ├── Makefile │ ├── README.rst │ ├── config │ │ └── config.yml │ ├── docker-compose.yml │ ├── motortwit │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── db.py │ │ ├── generate_data.py │ │ ├── main.py │ │ ├── routes.py │ │ ├── security.py │ │ ├── templates │ │ │ ├── layout.html │ │ │ ├── login.html │ │ │ ├── register.html │ │ │ └── timeline.html │ │ ├── utils.py │ │ └── views.py │ ├── pytest.ini │ ├── requirements.txt │ ├── setup.py │ ├── static │ │ └── css │ │ │ └── style.css │ └── tests │ │ └── test_motortwit.py ├── polls │ ├── Makefile │ ├── README.rst │ ├── aiohttpdemo_polls │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── db.py │ │ ├── main.py │ │ ├── middlewares.py │ │ ├── routes.py │ │ ├── settings.py │ │ ├── static │ │ │ ├── images │ │ │ │ └── background.png │ │ │ └── style.css │ │ ├── templates │ │ │ ├── 404.html │ │ │ ├── 500.html │ │ │ ├── base.html │ │ │ ├── detail.html │ │ │ ├── index.html │ │ │ └── results.html │ │ ├── typedefs.py │ │ ├── utils.py │ │ └── views.py │ ├── config │ │ ├── polls.yaml │ │ └── polls_test.yaml │ ├── init_db.py │ ├── pytest.ini │ ├── requirements-dev.txt │ ├── setup.py │ ├── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_integration.py │ └── tox.ini └── shortify │ ├── Makefile │ ├── README.rst │ ├── config │ └── config.yml │ ├── docker-compose.yml │ ├── pytest.ini │ ├── requirements.txt │ ├── setup.py │ ├── shortify │ ├── __init__.py │ ├── __main__.py │ ├── main.py │ ├── routes.py │ ├── templates │ │ └── index.html │ ├── utils.py │ └── views.py │ ├── static │ ├── css │ │ ├── custom.css │ │ └── materialize.min.css │ └── font │ │ └── roboto │ │ ├── Roboto-Regular.woff │ │ └── Roboto-Regular.woff2 │ └── tests │ ├── __init__.py │ ├── conftest.py │ └── test_shortify.py ├── docs ├── Makefile ├── _static │ ├── aiohttp-icon-128x128.png │ ├── aiohttp-icon-32x32.png │ ├── aiohttp-icon-64x64.png │ ├── aiohttp-icon-96x96.png │ ├── blog.png │ ├── chat.png │ ├── graph.gif │ ├── imagetagger.png │ ├── moderator.png │ ├── motortwit.png │ ├── polls.png │ ├── shorty.png │ └── slack_moderator.gif ├── aiohttp-icon.ico ├── aiohttp-icon.svg ├── conf.py ├── index.rst ├── make.bat ├── preparations.rst ├── spelling_wordlist.txt └── tutorial.rst └── requirements-dev.txt /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | enable-extensions = G 3 | max-doc-length = 90 4 | max-line-length = 90 5 | select = A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,B901,B902,B903,B950 6 | # E226: Missing whitespace around arithmetic operators can help group things together. 7 | # E501: Superseeded by B950 (from Bugbear) 8 | # E722: Superseeded by B001 (from Bugbear) 9 | # W503: Mutually exclusive with W504. 10 | # Q000: TODO: Code needs reformatting. 11 | ignore = E226,E501,E722,W503,Q000 12 | per-file-ignores = 13 | # S101: Pytest uses assert 14 | tests/*:S101 15 | 16 | # flake8-quotes 17 | inline-quotes = " 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | open-pull-requests-limit: 10 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | 14 | - package-ecosystem: "npm" 15 | directory: "/demos/graphql-demo/ui" 16 | schedule: 17 | interval: "daily" 18 | - package-ecosystem: "npm" 19 | directory: "/demos/moderator" 20 | schedule: 21 | interval: "daily" 22 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.3.0 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --squash "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | branches: 9 | - 'master' 10 | 11 | 12 | jobs: 13 | 14 | lint: 15 | name: Linter 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: 3.12 24 | cache: 'pip' 25 | cache-dependency-path: '**/requirements*.txt' 26 | - name: Install dependencies 27 | run: | 28 | make install 29 | - name: Run linters 30 | run: | 31 | make lint 32 | - name: Install libenchant 33 | run: sudo apt-get install libenchant-2-dev 34 | - name: Run docs spelling 35 | run: | 36 | make doc-spelling 37 | 38 | test: 39 | name: Test 40 | runs-on: ubuntu-latest 41 | services: 42 | postgres: 43 | image: postgres 44 | env: 45 | POSTGRES_PASSWORD: postgres 46 | # Set health checks to wait until postgres has started 47 | options: >- 48 | --health-cmd pg_isready 49 | --health-interval 10s 50 | --health-timeout 5s 51 | --health-retries 5 52 | ports: 53 | - 5432:5432 54 | redis: 55 | image: redis 56 | # Set health checks to wait until redis has started 57 | options: >- 58 | --health-cmd "redis-cli ping" 59 | --health-interval 10s 60 | --health-timeout 5s 61 | --health-retries 5 62 | ports: 63 | - 6379:6379 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | - name: Set up Python 68 | uses: actions/setup-python@v5 69 | with: 70 | python-version: 3.12 71 | cache: 'pip' 72 | cache-dependency-path: '**/requirements*.txt' 73 | - name: Install dependencies 74 | run: | 75 | make install 76 | - name: Run tests 77 | run: | 78 | PYTHONASYNCIODEBUG=1 make test 79 | 80 | doc: 81 | name: Documentation 82 | runs-on: ubuntu-latest 83 | steps: 84 | - name: Checkout 85 | uses: actions/checkout@v4 86 | - name: Install sphinx 87 | run: pip install sphinx 88 | - name: Documentation 89 | run: | 90 | make doc 91 | -------------------------------------------------------------------------------- /.github/workflows/post-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot post-update (rebuild moderator model) 2 | on: 3 | pull_request_target: 4 | action: [opened, synchronize, reopened] 5 | branches: 6 | - 'master' 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }} 10 | cancel-in-progress: true 11 | 12 | permissions: {} 13 | jobs: 14 | post-update: 15 | permissions: 16 | pull-requests: read # for gh pr checkout 17 | contents: write # to push code in repo (stefanzweifel/git-auto-commit-action) 18 | 19 | if: github.actor == 'dependabot[bot]' && contains(github.event.pull_request.title, 'scikit-learn') 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Generate token 23 | id: generate_token 24 | uses: tibdex/github-app-token@v2 25 | with: 26 | app_id: ${{ secrets.BOT_APP_ID }} 27 | private_key: ${{ secrets.BOT_PRIVATE_KEY }} 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | token: ${{ steps.generate_token.outputs.token }} 32 | - name: Login 33 | run: | 34 | echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token 35 | - name: Checkout 36 | run: | 37 | gh pr checkout ${{ github.event.pull_request.number }} 38 | - name: Setup Python 39 | uses: actions/setup-python@v5 40 | cache: 'pip' 41 | cache-dependency-path: '**/requirements*.txt' 42 | - name: Download train.csv 43 | # Original file can be found at: https://www.kaggle.com/code/piumallick/toxic-comments-sentiment-analysis/input?select=train.csv.zip 44 | run: curl "$MODERATOR_TRAINING_URL" > train.csv 45 | - name: Rebuild model 46 | run: PYTHONPATH='demos/moderator' python -m moderator.build_model train.csv 47 | - name: Commit and push if needed 48 | uses: stefanzweifel/git-auto-commit-action@v5 49 | with: 50 | commit_message: Rebuild moderator model 51 | file_pattern: 'demos/moderator/model/pipeline.dat' 52 | -------------------------------------------------------------------------------- /.github/workflows/shiftleft-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow integrates Scan with GitHub's code scanning feature 2 | # Scan is a free open-source security tool for modern DevOps teams from ShiftLeft 3 | # Visit https://slscan.io/en/latest/integrations/code-scan for help 4 | name: SL Scan 5 | 6 | # This section configures the trigger for the workflow. Feel free to customize depending on your convention 7 | on: [push, pull_request] 8 | 9 | jobs: 10 | Scan-Build: 11 | # Scan runs on ubuntu, mac and windows 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | # Instructions 16 | # 1. Setup JDK, Node.js, Python etc depending on your project type 17 | # 2. Compile or build the project before invoking scan 18 | # Example: mvn compile, or npm install or pip install goes here 19 | # 3. Invoke Scan with the github token. Leave the workspace empty to use relative url 20 | 21 | - name: Perform Scan 22 | uses: ShiftLeftSecurity/scan-action@master 23 | env: 24 | WORKSPACE: "" 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | SCAN_AUTO_BUILD: true 27 | with: 28 | output: reports 29 | # Scan auto-detects the languages in your project. To override uncomment the below variable and set the type 30 | # type: credscan,java 31 | # type: python 32 | 33 | - name: Upload report 34 | uses: github/codeql-action/upload-sarif@v3 35 | with: 36 | sarif_file: reports 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | node_modules 103 | 104 | 105 | tags 106 | cscope.out 107 | cscope.files 108 | 109 | # idea 110 | .idea/ 111 | 112 | # dependencies 113 | node_modules/ 114 | 115 | # testing 116 | /coverage 117 | 118 | # production 119 | build/ 120 | 121 | # misc 122 | npm-debug.log* 123 | yarn-debug.log* 124 | yarn-error.log* 125 | .DS_Store 126 | 127 | # pytest 128 | .pytest_cache/ 129 | 130 | .vscode/ 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017-2020 aio-libs collaboration. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | @rm -rf `find . -name __pycache__` 3 | @make -C docs clean 4 | 5 | lint: 6 | @flake8 demos 7 | 8 | test: 9 | @pytest demos/blog 10 | @pytest demos/chat 11 | @pytest demos/graphql-demo 12 | @pytest demos/imagetagger 13 | @pytest demos/moderator 14 | @SLACK_BOT_TOKEN=xxx GIPHY_API_KEY=xxx pytest demos/moderator_bot/tests 15 | @pytest demos/motortwit 16 | @pytest demos/polls 17 | @pytest demos/shortify 18 | 19 | ci: lint test doc-spelling 20 | 21 | doc: 22 | @make -C docs html SPHINXOPTS="-W -E" 23 | @echo "open file://`pwd`/docs/_build/html/index.html" 24 | 25 | doc-spelling: 26 | @make -C docs spelling SPHINXOPTS="-W -E" 27 | 28 | install: 29 | pip install -U pip setuptools cython 30 | pip install -r requirements-dev.txt 31 | pip install -r demos/blog/requirements.txt 32 | pip install -r demos/chat/requirements.txt 33 | pip install -r demos/graphql-demo/requirements-dev.txt 34 | pip install -r demos/imagetagger/requirements.txt 35 | pip install -r demos/moderator/requirements-dev.txt 36 | pip install -r demos/moderator_bot/requirements-dev.txt 37 | pip install -r demos/motortwit/requirements.txt 38 | pip install -r demos/polls/requirements-dev.txt 39 | pip install -r demos/shortify/requirements.txt 40 | 41 | .PHONY: clean doc 42 | -------------------------------------------------------------------------------- /demos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/__init__.py -------------------------------------------------------------------------------- /demos/blog/README.rst: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | 4 | Clone the repo, create virtualenv if necessary. 5 | 6 | Operate from base folder [../aiohttp-demos/demos/blog]. 7 | 8 | To start the demo application you need running Postgres server. 9 | In case you have it already - good. 10 | 11 | But if you want neither to use it for experiments nor to stop the server: 12 | - update DB_PORT in config files 13 | - use desired port in following example commands e.g. like so:: 14 | 15 | $ export DB_PORT=5433 16 | 17 | Run db servers:: 18 | 19 | $ docker run --rm -d -p $DB_PORT:5432 postgres:10 20 | $ docker run --rm -d -p 6379:6379 redis 21 | 22 | 23 | Create db with tables and sample data:: 24 | 25 | $ python db_helpers.py -a 26 | 27 | Check db for created data:: 28 | 29 | $ psql -h localhost -p $DB_PORT -U postgres -d aiohttpdemo_blog -c "select * from posts" 30 | 31 | Run server:: 32 | 33 | $ python aiohttpdemo_blog/main.py -c config/user_config.toml 34 | 35 | 36 | (example creds: Bob/bob) 37 | 38 | Testing 39 | ======= 40 | 41 | Run tests:: 42 | 43 | $ pytest tests/test_stuff.py 44 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/blog/aiohttpdemo_blog/__init__.py -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/db_auth.py: -------------------------------------------------------------------------------- 1 | from aiohttp_security.abc import AbstractAuthorizationPolicy 2 | 3 | from aiohttpdemo_blog import db 4 | from aiohttpdemo_blog.typedefs import db_key 5 | 6 | 7 | class DBAuthorizationPolicy(AbstractAuthorizationPolicy): 8 | def __init__(self, app): 9 | self.app = app 10 | 11 | async def authorized_userid(self, identity): 12 | async with self.app[db_key]() as sess: 13 | user = await db.get_user_by_name(sess, identity) 14 | if user: 15 | return identity 16 | return None 17 | 18 | async def permits(self, identity, permission, context=None): 19 | if identity is None: 20 | return False 21 | return True 22 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/forms.py: -------------------------------------------------------------------------------- 1 | from aiohttpdemo_blog import db 2 | from aiohttpdemo_blog.security import check_password_hash 3 | 4 | 5 | async def validate_login_form(conn, form): 6 | 7 | username = form['username'] 8 | password = form['password'] 9 | 10 | if not username: 11 | return 'username is required' 12 | if not password: 13 | return 'password is required' 14 | 15 | user = await db.get_user_by_name(conn, username) 16 | 17 | if not user: 18 | return 'Invalid username' 19 | if not check_password_hash(password, user.password_hash): 20 | return 'Invalid password' 21 | 22 | return None 23 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/routes.py: -------------------------------------------------------------------------------- 1 | from aiohttpdemo_blog.views import index, login, logout, create_post 2 | 3 | 4 | def setup_routes(app): 5 | app.router.add_get('/', index, name='index') 6 | app.router.add_get('/login', login, name='login') 7 | app.router.add_post('/login', login, name='login') 8 | app.router.add_get('/logout', logout, name='logout') 9 | app.router.add_get('/create', create_post, name='create-post') 10 | app.router.add_post('/create', create_post, name='create-post') 11 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/security.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | 3 | 4 | def generate_password_hash(password): 5 | password_bin = password.encode('utf-8') 6 | hashed = bcrypt.hashpw(password_bin, bcrypt.gensalt()) 7 | return hashed.decode('utf-8') 8 | 9 | 10 | def check_password_hash(plain_password, password_hash): 11 | plain_password_bin = plain_password.encode('utf-8') 12 | password_hash_bin = password_hash.encode('utf-8') 13 | is_correct = bcrypt.checkpw(plain_password_bin, password_hash_bin) 14 | return is_correct 15 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/settings.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import pytoml as toml 3 | 4 | BASE_DIR = pathlib.Path(__file__).parent.parent 5 | PACKAGE_NAME = 'aiohttpdemo_blog' 6 | 7 | 8 | def load_config(path): 9 | with open(path) as f: 10 | conf = toml.load(f) 11 | return conf 12 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if title %} 4 | {{ title }} - Microblog 5 | {% else %} 6 | Welcome to Microblog 7 | {% endif %} 8 | 9 | 10 | 11 |
12 | Microblog: 13 | Home 14 | {% if current_user.is_anonymous %} 15 | Login 16 | {% else %} 17 | Logout 18 | Write 19 | {% endif %} 20 |
21 |
22 | 23 | {% block content %}{% endblock %} 24 | 25 | 26 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/templates/create_post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Write something

6 | 7 |
8 |

9 | 10 |

11 | 12 |
13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

Hi, {{ user.username }}!

5 | {% for post in posts %} 6 |

[{{ post.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}] {{ post.user.username }} posted: {{ post.body }}

7 | {% endfor %} 8 | {% endblock %} -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Sign In

6 | 7 | {% if error %}
Error: {{ error }}
{% endif %} 8 |
9 |

10 |
11 | 12 |

13 |

14 |
15 | 16 |

17 | 18 |
19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/typedefs.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from aiohttp import web 4 | from sqlalchemy.ext.asyncio import async_sessionmaker 5 | 6 | config_key = web.AppKey("config_key", dict[str, Any]) 7 | db_key = web.AppKey("db_key", async_sessionmaker) 8 | -------------------------------------------------------------------------------- /demos/blog/aiohttpdemo_blog/views.py: -------------------------------------------------------------------------------- 1 | import aiohttp_jinja2 2 | from aiohttp import web 3 | from aiohttp_security import remember, forget, authorized_userid 4 | 5 | from aiohttpdemo_blog import db 6 | from aiohttpdemo_blog.forms import validate_login_form 7 | from aiohttpdemo_blog.typedefs import db_key 8 | 9 | 10 | def redirect(router, route_name): 11 | location = router[route_name].url_for() 12 | return web.HTTPFound(location) 13 | 14 | 15 | @aiohttp_jinja2.template('index.html') 16 | async def index(request): 17 | username = await authorized_userid(request) 18 | if not username: 19 | raise redirect(request.app.router, 'login') 20 | 21 | async with request.app[db_key]() as sess: 22 | current_user = await db.get_user_by_name(sess, username) 23 | posts = await db.get_posts_with_joined_users(sess) 24 | 25 | return {'user': current_user, 'posts': posts} 26 | 27 | 28 | @aiohttp_jinja2.template('login.html') 29 | async def login(request): 30 | username = await authorized_userid(request) 31 | if username: 32 | raise redirect(request.app.router, 'index') 33 | 34 | if request.method == 'POST': 35 | form = await request.post() 36 | 37 | async with request.app[db_key]() as sess: 38 | error = await validate_login_form(sess, form) 39 | 40 | if error: 41 | return {'error': error} 42 | else: 43 | response = redirect(request.app.router, 'index') 44 | 45 | user = await db.get_user_by_name(sess, form['username']) 46 | await remember(request, response, user.username) 47 | 48 | raise response 49 | 50 | return {} 51 | 52 | 53 | async def logout(request): 54 | response = redirect(request.app.router, 'login') 55 | await forget(request, response) 56 | return response 57 | 58 | 59 | @aiohttp_jinja2.template('create_post.html') 60 | async def create_post(request): 61 | username = await authorized_userid(request) 62 | if not username: 63 | raise redirect(request.app.router, 'login') 64 | 65 | if request.method == 'POST': 66 | form = await request.post() 67 | 68 | async with request.app[db_key].begin() as sess: 69 | current_user = await db.get_user_by_name(sess, username) 70 | sess.add(db.Posts(body=form["body"], user_id=current_user.id)) 71 | raise redirect(request.app.router, 'index') 72 | 73 | return {} 74 | -------------------------------------------------------------------------------- /demos/blog/config/admin_config.toml: -------------------------------------------------------------------------------- 1 | [database] 2 | 3 | DB_HOST = 'localhost' 4 | DB_PORT = 5432 5 | 6 | DB_NAME = 'postgres' 7 | DB_USER = 'postgres' 8 | DB_PASS = 'postgres' 9 | 10 | [redis] 11 | 12 | REDIS_HOST = 'localhost' 13 | REDIS_PORT = 6379 14 | -------------------------------------------------------------------------------- /demos/blog/config/test_config.toml: -------------------------------------------------------------------------------- 1 | [database] 2 | 3 | DB_HOST = 'localhost' 4 | DB_PORT = 5432 5 | 6 | DB_NAME = 'test' 7 | DB_USER = 'test' 8 | DB_PASS = 'test' 9 | 10 | [redis] 11 | 12 | REDIS_HOST = 'localhost' 13 | REDIS_PORT = 6379 14 | -------------------------------------------------------------------------------- /demos/blog/config/user_config.toml: -------------------------------------------------------------------------------- 1 | [database] 2 | 3 | DB_HOST = 'localhost' 4 | DB_PORT = 5432 5 | 6 | DB_NAME = 'aiohttpdemo_blog' 7 | DB_USER = 'aiohttpdemo_user' 8 | DB_PASS = 'aiohttpdemo_pass' 9 | 10 | [redis] 11 | 12 | REDIS_HOST = 'localhost' 13 | REDIS_PORT = 6379 14 | -------------------------------------------------------------------------------- /demos/blog/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | # show 10 slowest invocations: 4 | --durations=10 5 | 6 | # a bit of verbosity doesn't hurt: 7 | -v 8 | 9 | # report all the things == -rxXs: 10 | -ra 11 | 12 | # show values of the local vars in errors: 13 | --showlocals 14 | asyncio_mode = auto 15 | filterwarnings = 16 | error 17 | testpaths = tests/ 18 | xfail_strict = true 19 | -------------------------------------------------------------------------------- /demos/blog/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.11.18 2 | aiohttp-jinja2==1.6 3 | aiohttp-security[session]==0.5.0 4 | asyncpg==0.30.0 5 | bcrypt==4.3.0 6 | pytoml==0.1.21 7 | redis==6.1.0 8 | SQLalchemy==2.0.41 9 | 10 | # testing 11 | pytest==8.3.5 12 | pytest-aiohttp==1.1.0 13 | pytest-asyncio==0.26.0 14 | flake8==7.2.0 15 | -------------------------------------------------------------------------------- /demos/blog/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | install_requires = ( 5 | 'aiohttp', 6 | 'aiohttp-jinja2', 7 | 'bcrypt', 8 | 'pytoml', 9 | 'aiohttp_security[session]', 10 | 'redis>=4.2', 11 | 'sqlalchemy', 12 | 'asyncpg', 13 | 'asyncpgsa', 14 | ) 15 | 16 | setup( 17 | name='aiohttpdemo-blog', 18 | version='0.2', 19 | install_requires=install_requires, 20 | packages=("aiohttpdemo_blog",) 21 | ) 22 | -------------------------------------------------------------------------------- /demos/blog/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/blog/tests/__init__.py -------------------------------------------------------------------------------- /demos/blog/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aiohttpdemo_blog.main import init_app 4 | from aiohttpdemo_blog.settings import load_config, BASE_DIR 5 | from db_helpers import ( 6 | setup_db, 7 | teardown_db, 8 | create_tables, 9 | create_sample_data, 10 | drop_tables, 11 | ) 12 | 13 | 14 | @pytest.fixture 15 | async def client(aiohttp_client): 16 | config = load_config(BASE_DIR / "config" / "test_config.toml") 17 | app = await init_app(config) 18 | return await aiohttp_client(app) 19 | 20 | 21 | @pytest.fixture(scope="session") 22 | async def database(): 23 | admin_db_config = load_config(BASE_DIR / "config" / "admin_config.toml")["database"] 24 | test_db_config = load_config(BASE_DIR / "config" / "test_config.toml")["database"] 25 | 26 | await setup_db(executor_config=admin_db_config, target_config=test_db_config) 27 | yield 28 | await teardown_db(executor_config=admin_db_config, target_config=test_db_config) 29 | 30 | 31 | @pytest.fixture 32 | async def tables_and_data(database): 33 | test_db_config = load_config(BASE_DIR / "config" / "test_config.toml")["database"] 34 | 35 | await create_tables(target_config=test_db_config) 36 | await create_sample_data(target_config=test_db_config) 37 | 38 | yield 39 | 40 | await drop_tables(target_config=test_db_config) 41 | -------------------------------------------------------------------------------- /demos/blog/tests/test_stuff.py: -------------------------------------------------------------------------------- 1 | from aiohttpdemo_blog.forms import validate_login_form 2 | from aiohttpdemo_blog.security import ( 3 | generate_password_hash, 4 | check_password_hash 5 | ) 6 | from aiohttpdemo_blog.typedefs import db_key 7 | 8 | 9 | def test_security(): 10 | user_password = 'qwer' 11 | hashed = generate_password_hash(user_password) 12 | assert check_password_hash(user_password, hashed) 13 | 14 | 15 | async def test_index_view(tables_and_data, client): 16 | async with client.get("/") as resp: 17 | assert resp.status == 200 18 | 19 | 20 | async def test_login_form(tables_and_data, client): 21 | invalid_form = { 22 | 'username': 'Joe', 23 | 'password': '123' 24 | } 25 | valid_form = { 26 | 'username': 'Adam', 27 | 'password': 'adam' 28 | } 29 | async with client.server.app[db_key]() as sess: 30 | error = await validate_login_form(sess, invalid_form) 31 | assert error 32 | 33 | no_error = await validate_login_form(sess, valid_form) 34 | assert not no_error 35 | 36 | 37 | async def test_login_view(tables_and_data, client): 38 | invalid_form = { 39 | 'username': 'Joe', 40 | 'password': '123' 41 | } 42 | valid_form = { 43 | 'username': 'Adam', 44 | 'password': 'adam' 45 | } 46 | 47 | async with client.post("/login", data=invalid_form) as resp: 48 | assert resp.status == 200 49 | assert "Invalid username" in await resp.text() 50 | 51 | async with await client.post("/login", data=valid_form) as resp: 52 | assert resp.status == 200 53 | assert "Hi, Adam!" in await resp.text() 54 | -------------------------------------------------------------------------------- /demos/chat/README.rst: -------------------------------------------------------------------------------- 1 | Chat Demo 2 | ========= 3 | 4 | Chat with websockets. 5 | 6 | Installation 7 | ============ 8 | 9 | Clone repo and install library:: 10 | 11 | $ git clone git@github.com:aio-libs/aiohttp-demos.git 12 | $ cd aiohttp-demos 13 | 14 | Install the app:: 15 | 16 | $ cd demos/chat 17 | $ pip install -e . 18 | 19 | Run application:: 20 | 21 | $ cd aiohttpdemo_chat 22 | $ python main.py 23 | 24 | Open browser:: 25 | 26 | http://127.0.0.1:8080 27 | 28 | Open several tabs, make them visible at the same time (to see messages sent from other tabs 29 | without page refresh). 30 | 31 | 32 | Requirements 33 | ============ 34 | * aiohttp_ 35 | * aiohttp_jinja2_ 36 | 37 | 38 | .. _Python: https://www.python.org 39 | .. _aiohttp: https://github.com/aio-libs/aiohttp 40 | .. _aiohttp_jinja2: https://github.com/aio-libs/aiohttp_jinja2 41 | -------------------------------------------------------------------------------- /demos/chat/aiohttpdemo_chat/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.1' 2 | -------------------------------------------------------------------------------- /demos/chat/aiohttpdemo_chat/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import jinja2 4 | 5 | import aiohttp_jinja2 6 | from aiohttp import web 7 | from aiohttpdemo_chat.views import index, ws_key 8 | 9 | 10 | async def init_app(): 11 | 12 | app = web.Application() 13 | 14 | app[ws_key] = {} 15 | 16 | app.on_shutdown.append(shutdown) 17 | 18 | aiohttp_jinja2.setup( 19 | app, loader=jinja2.PackageLoader('aiohttpdemo_chat', 'templates')) 20 | 21 | app.router.add_get('/', index) 22 | 23 | return app 24 | 25 | 26 | async def shutdown(app): 27 | for ws in app[ws_key].values(): 28 | await ws.close() 29 | app[ws_key].clear() 30 | 31 | 32 | async def get_app(): 33 | """Used by aiohttp-devtools for local development.""" 34 | import aiohttp_debugtoolbar 35 | app = await init_app() 36 | aiohttp_debugtoolbar.setup(app) 37 | return app 38 | 39 | 40 | def main(): 41 | logging.basicConfig(level=logging.DEBUG) 42 | 43 | app = init_app() 44 | web.run_app(app) 45 | 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /demos/chat/aiohttpdemo_chat/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import aiohttp 4 | import aiohttp_jinja2 5 | from aiohttp import web 6 | from faker import Faker 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | ws_key = web.AppKey("ws_key", dict[str, web.WebSocketResponse]) 11 | 12 | 13 | def get_random_name(): 14 | fake = Faker() 15 | return fake.name() 16 | 17 | 18 | async def index(request): 19 | ws_current = web.WebSocketResponse() 20 | ws_ready = ws_current.can_prepare(request) 21 | if not ws_ready.ok: 22 | return aiohttp_jinja2.render_template('index.html', request, {}) 23 | 24 | await ws_current.prepare(request) 25 | 26 | name = get_random_name() 27 | log.info('%s joined.', name) 28 | 29 | await ws_current.send_json({'action': 'connect', 'name': name}) 30 | 31 | for ws in request.app[ws_key].values(): 32 | await ws.send_json({'action': 'join', 'name': name}) 33 | request.app[ws_key][name] = ws_current 34 | 35 | while True: 36 | msg = await ws_current.receive() 37 | 38 | if msg.type == aiohttp.WSMsgType.text: 39 | for ws in request.app[ws_key].values(): 40 | if ws is not ws_current: 41 | await ws.send_json( 42 | {'action': 'sent', 'name': name, 'text': msg.data}) 43 | else: 44 | break 45 | 46 | del request.app[ws_key][name] 47 | log.info('%s disconnected.', name) 48 | for ws in request.app[ws_key].values(): 49 | await ws.send_json({'action': 'disconnect', 'name': name}) 50 | 51 | return ws_current 52 | -------------------------------------------------------------------------------- /demos/chat/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | # show 10 slowest invocations: 4 | --durations=10 5 | 6 | # a bit of verbosity doesn't hurt: 7 | -v 8 | 9 | # report all the things == -rxXs: 10 | -ra 11 | 12 | # show values of the local vars in errors: 13 | --showlocals 14 | asyncio_mode = auto 15 | filterwarnings = 16 | error 17 | testpaths = tests/ 18 | xfail_strict = true 19 | -------------------------------------------------------------------------------- /demos/chat/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.11.18 2 | aiohttp-jinja2==1.6 3 | faker==37.3.0 4 | -------------------------------------------------------------------------------- /demos/chat/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | def read_version(): 8 | regexp = re.compile(r"^__version__\W*=\W*'([\d.abrc]+)'") 9 | init_py = os.path.join(os.path.dirname(__file__), 10 | 'aiohttpdemo_chat', '__init__.py') 11 | with open(init_py) as f: 12 | for line in f: 13 | match = regexp.match(line) 14 | if match is not None: 15 | return match.group(1) 16 | msg = 'Cannot find version in aiohttpdemo_chat/__init__.py' 17 | raise RuntimeError(msg) 18 | 19 | 20 | install_requires = ['aiohttp>=3.6', 21 | 'aiohttp_jinja2', 22 | 'faker'] 23 | 24 | 25 | setup(name='aiohttpdemo_chat', 26 | version=read_version(), 27 | description='Chat example from aiohttp', 28 | platforms=['POSIX'], 29 | packages=find_packages(), 30 | include_package_data=True, 31 | install_requires=install_requires, 32 | zip_safe=False) 33 | -------------------------------------------------------------------------------- /demos/chat/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/chat/tests/__init__.py -------------------------------------------------------------------------------- /demos/chat/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from aiohttpdemo_chat.main import init_app 3 | 4 | 5 | @pytest.fixture 6 | async def client(aiohttp_client): 7 | app = await init_app() 8 | return await aiohttp_client(app) 9 | -------------------------------------------------------------------------------- /demos/chat/tests/test_chat.py: -------------------------------------------------------------------------------- 1 | async def test_msg_sending(client): 2 | ws1 = await client.ws_connect('/') 3 | ws2 = await client.ws_connect('/') 4 | 5 | ack_msg1 = await ws1.receive() 6 | assert ack_msg1.json()['action'] == 'connect' 7 | ack_msg2 = await ws2.receive() 8 | assert ack_msg2.json()['action'] == 'connect' 9 | 10 | text_to_send = 'hi all' 11 | await ws1.send_str(text_to_send) 12 | received_msg = await ws2.receive() 13 | received_dict = received_msg.json() 14 | 15 | assert set(received_dict) == {'action', 'name', 'text'} 16 | assert received_dict['text'] == text_to_send 17 | -------------------------------------------------------------------------------- /demos/graphql-demo/Makefile: -------------------------------------------------------------------------------- 1 | export PYTHONPATH=. 2 | 3 | PROJECT_NAME=graph 4 | 5 | all: run 6 | 7 | run: 8 | @python -m $(PROJECT_NAME) 9 | 10 | mypy: 11 | @mypy $(PROJECT_NAME) 12 | 13 | lint: 14 | @flake8 $(PROJECT_NAME) 15 | 16 | test: lint 17 | @py.test 18 | 19 | start_database: 20 | @docker-compose up -d postgres 21 | 22 | start_redis: 23 | @docker-compose up -d redis 24 | 25 | prepare_database: 26 | @python prepare_database.py 27 | 28 | run_ui: 29 | @cd ui && yarn start 30 | -------------------------------------------------------------------------------- /demos/graphql-demo/README.md: -------------------------------------------------------------------------------- 1 | # GraphQl Messenger 2 | 3 | The simple realization of GraphQl api. 4 | - query 5 | - mutations 6 | - subscriptions 7 | 8 | ![Image of Application](/docs/_static/graph.gif) 9 | 10 | 11 | ### Install requirements: 12 | ``` 13 | $ cd graphql-demo 14 | $ pip install -r requirements-dev.txt 15 | $ pip install -e . 16 | ``` 17 | 18 | ### Run application: 19 | ``` 20 | $ make start_database 21 | $ make start_redis 22 | $ make prepare_database 23 | $ make 24 | ``` 25 | ### Open in browser: 26 | ``` 27 | $ open http://0.0.0.0:8080/ 28 | $ open http://0.0.0.0:8080/graphiql 29 | ``` 30 | 31 | ### Others 32 | 33 | ``` 34 | make test # running tests 35 | make lint # running the flake8 36 | make mypy # running the types checking by mypy 37 | 38 | ``` 39 | 40 | ### Requirements 41 | - aiohttp 42 | - aiopg 43 | - aioredis 44 | - graphene 45 | -------------------------------------------------------------------------------- /demos/graphql-demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/graphql-demo/__init__.py -------------------------------------------------------------------------------- /demos/graphql-demo/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | postgres: 5 | image: postgres:10 6 | environment: 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | - POSTGRES_DB=postgres 10 | ports: 11 | - 5432:5432 12 | 13 | redis: 14 | image: redis:4 15 | ports: 16 | - 6379:6379 17 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 79 3 | exclude = env/* 4 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.1' 2 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/__main__.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | from .app import init_app 4 | 5 | 6 | def main(): 7 | app = init_app() 8 | web.run_app(app) 9 | 10 | 11 | if __name__ == '__main__': 12 | main() 13 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/graphql-demo/graph/api/__init__.py -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/dataloaders.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from aiodataloader import DataLoader 4 | from graph.auth.db_utils import select_users 5 | from graph.auth.models import User 6 | from sqlalchemy.ext.asyncio import async_sessionmaker 7 | 8 | __all__ = ("UserDataLoader",) 9 | 10 | 11 | class BaseAIODataLoader(DataLoader): 12 | """The base data loader for aiohttp. 13 | 14 | It need create when application initialization with current db engine. 15 | """ 16 | 17 | engine: Any = None 18 | 19 | def __init__(self, engine, *args, **kwargs): 20 | super().__init__(*args, **kwargs) 21 | self.engine = engine 22 | self.session = async_sessionmaker(self.engine, expire_on_commit=False) 23 | 24 | def sorted_by_keys(self, items: list[User], keys: List[int]) -> list[User]: 25 | """Help ordering of returned items In `aiodataloader`.""" 26 | items_dict = {key: value for key, value in zip(sorted(set(keys)), items)} 27 | 28 | return [items_dict[key] for key in keys] 29 | 30 | 31 | class UserDataLoader(BaseAIODataLoader): 32 | """Simple user data loader. 33 | 34 | Should be used everywhere, when it is possible problem N + 1 requests. 35 | """ 36 | 37 | async def batch_load_fn(self, keys: List[int]) -> list[User]: 38 | async with self.session.begin() as sess: 39 | response = await select_users(sess, keys) 40 | 41 | return self.sorted_by_keys(response, keys) 42 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/graphql-demo/graph/api/models/__init__.py -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/models/room.py: -------------------------------------------------------------------------------- 1 | 2 | import graphene 3 | from graph.api.models.user import User 4 | from graph.chat.db_utils import select_messages_by_room_id 5 | from graphql import ResolveInfo 6 | 7 | __all__ = ("Message", "Room") 8 | 9 | 10 | class Message(graphene.ObjectType): 11 | """Main object that representation data of user message.""" 12 | 13 | id = graphene.Int( 14 | description="An id of message, it's unique for all message", 15 | ) 16 | body = graphene.String(description="An text of message") 17 | favouriteCount = graphene.Int( 18 | description="A count of user who favorited current message", 19 | ) 20 | 21 | owner = graphene.Field( 22 | User, 23 | description="An creator of message", 24 | ) 25 | 26 | async def resolve_owner(self, info: ResolveInfo): 27 | app = info.context["request"].app 28 | owner = await app["loaders"].users.load(self.owner_id) 29 | assert owner is not None, f"Owner was None from {app=} and {info=}" 30 | return owner 31 | 32 | 33 | class Room(graphene.ObjectType): 34 | """Point where users can have conversations.""" 35 | 36 | id = graphene.Int( 37 | description="An id of room, it's unique for all rooms", 38 | ) 39 | name = graphene.String( 40 | description="A name of room", 41 | ) 42 | 43 | owner = graphene.Field( 44 | User, 45 | description="The user who create the current room", 46 | ) 47 | messages = graphene.List( 48 | Message, 49 | description="The messages of the current room", 50 | ) 51 | 52 | async def resolve_owner(self, info: ResolveInfo): 53 | app = info.context["request"].app 54 | owner = await app["loaders"].users.load(self.owner_id) 55 | assert owner is not None, f"Owner was None from {app=} and {info=}" 56 | return owner 57 | 58 | async def resolve_messages(self, info: ResolveInfo): 59 | app = info.context["request"].app 60 | 61 | async with app["db"].begin() as sess: 62 | return await select_messages_by_room_id(sess, self.id) 63 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/models/user.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | __all__ = ['User', ] 5 | 6 | 7 | class User(graphene.ObjectType): 8 | """Individual's account on current api that can have conversations.""" 9 | id = graphene.Int( 10 | description='A id of user', 11 | ) 12 | username = graphene.String( 13 | description='A username of user', 14 | ) 15 | email = graphene.String( 16 | description='A username of user', 17 | ) 18 | avatar_url = graphene.String( 19 | description='A main user`s photo', 20 | ) 21 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/mutations/__init__.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from graph.api.mutations.messages import ( 4 | AddMessageMutation, 5 | RemoveMessageMutation, 6 | StartTypingMessageMutation, 7 | ) 8 | 9 | 10 | class Mutation(graphene.ObjectType): 11 | """Main GraphQL mutation point.""" 12 | 13 | add_message = AddMessageMutation.Field() 14 | remove_message = RemoveMessageMutation.Field() 15 | start_typing = StartTypingMessageMutation.Field() 16 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/mutations/messages.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from graph.auth.db_utils import select_user 4 | from graph.chat.db_utils import ( 5 | create_message, 6 | delete_message, 7 | ) 8 | 9 | 10 | __all__ = [ 11 | "AddMessageMutation", 12 | "RemoveMessageMutation", 13 | "StartTypingMessageMutation", 14 | ] 15 | 16 | 17 | class AddMessageMutation(graphene.Mutation): 18 | """Provide interface for create new messages.""" 19 | 20 | class Arguments: 21 | room_id = graphene.Int() 22 | owner_id = graphene.Int() 23 | body = graphene.String() 24 | 25 | is_created = graphene.Boolean() 26 | 27 | async def mutate(self, info, room_id: int, owner_id: int, body: str): 28 | app = info.context["request"].app 29 | 30 | async with app["db"].begin() as sess: 31 | message = await create_message(sess, room_id, owner_id, body) 32 | owner = await select_user(sess, owner_id) 33 | 34 | await app["redis_pub"].publish_json( 35 | f"chat:{room_id}", 36 | { 37 | "body": body, 38 | "id": message.id, 39 | "username": owner.username, 40 | "user_id": owner.id, 41 | }, 42 | ) 43 | 44 | return AddMessageMutation(is_created=True) 45 | 46 | 47 | class RemoveMessageMutation(graphene.Mutation): 48 | """Provide interface for create new message by id.""" 49 | 50 | class Arguments: 51 | id = graphene.Int() 52 | 53 | is_removed = graphene.Boolean() 54 | 55 | async def mutate(self, info, id: int): 56 | app = info.context["request"].app 57 | 58 | async with app["db"].begin() as sess: 59 | await delete_message(sess, id) 60 | 61 | return RemoveMessageMutation(is_removed=True) 62 | 63 | 64 | class StartTypingMessageMutation(graphene.Mutation): 65 | """Provide interface for set info about start typing new message.""" 66 | 67 | class Arguments: 68 | room_id = graphene.Int() 69 | user_id = graphene.Int() 70 | 71 | is_success = graphene.Boolean() 72 | 73 | async def mutate(self, info, room_id: int, user_id: int): 74 | app = info.context["request"].app 75 | 76 | async with app["db"].begin() as sess: 77 | user = await select_user(sess, user_id) 78 | 79 | await app["redis_pub"].publish_json( 80 | f"chat:typing:{room_id}", 81 | {"username": user.username, "id": user.id}, 82 | ) 83 | 84 | return StartTypingMessageMutation(is_success=True) 85 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/queries/__init__.py: -------------------------------------------------------------------------------- 1 | from graph.api.queries.user import UserQuery 2 | from graph.api.queries.rooms import RoomsQuery 3 | 4 | 5 | class Query(UserQuery, RoomsQuery): 6 | """Main GraphQL query point.""" 7 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/queries/rooms.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graph.api.models.room import Room 3 | from graph.chat.db_utils import select_room, select_rooms 4 | from graphql import ResolveInfo 5 | 6 | __all__ = ("RoomsQuery",) 7 | 8 | 9 | class RoomsQuery(graphene.ObjectType): 10 | rooms = graphene.List( 11 | Room, 12 | description="A list of all available rooms", 13 | ) 14 | room = graphene.Field( 15 | Room, 16 | id=graphene.Argument(graphene.Int), 17 | description="A room with given id", 18 | ) 19 | 20 | async def resolve_rooms(self, info: ResolveInfo) -> list[list[Room]]: 21 | app = info.context["request"].app 22 | sessionmaker = app["db"] 23 | 24 | try: 25 | async with sessionmaker() as sess: 26 | return await select_rooms(sess) 27 | except Exception as exc: 28 | raise TypeError(repr(sessionmaker)) from exc 29 | 30 | async def resolve_room(self, info: ResolveInfo, id: int) -> list[Room]: 31 | app = info.context["request"].app 32 | 33 | async with app["db"]() as sess: 34 | return await select_room(sess, id) 35 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/queries/user.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from graph.api.models.user import User 4 | 5 | 6 | class UserQuery(graphene.ObjectType): 7 | viewer = graphene.Field( 8 | User, 9 | description='A current user', 10 | ) 11 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/subscriptions/__init__.py: -------------------------------------------------------------------------------- 1 | from graph.api.subscriptions.messages import MessageSubscription 2 | 3 | 4 | class Subscription(MessageSubscription): 5 | """Main GraphQL subscription point.""" 6 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/subscriptions/messages.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | 4 | import graphene 5 | from graph.api.models.user import User 6 | from graphql import ResolveInfo 7 | 8 | 9 | class RandomType(graphene.ObjectType): 10 | """Random type. Need for test.""" 11 | 12 | seconds = graphene.Int() 13 | random_int = graphene.Int() 14 | 15 | 16 | class MessageAdded(graphene.ObjectType): 17 | """The simple type for representation message response in subscription.""" 18 | 19 | id = graphene.Int(description="The id of message") 20 | body = graphene.String(description="The text of message") 21 | owner = graphene.Field(User, description="The owner of current message") 22 | 23 | 24 | class StartTyping(graphene.ObjectType): 25 | """Simple type for representing info connected with starting of typing 26 | new message by some user. 27 | """ 28 | 29 | user = graphene.Field(User, description="The user why are typing right now") 30 | 31 | 32 | class MessageSubscription(graphene.ObjectType): 33 | """Subscriptions for all actions connected with messages.""" 34 | 35 | random_int = graphene.Field(RandomType) 36 | typing_start = graphene.Field(StartTyping, room_id=graphene.Argument(graphene.Int)) 37 | message_added = graphene.Field( 38 | MessageAdded, room_id=graphene.Argument(graphene.Int) 39 | ) 40 | 41 | async def resolve_random_int(self, info: ResolveInfo) -> RandomType: 42 | i = 0 43 | while True: 44 | yield RandomType(seconds=i, random_int=random.randint(0, 500)) 45 | await asyncio.sleep(1.0) 46 | i += 1 47 | 48 | async def resolve_message_added( 49 | self, 50 | info: ResolveInfo, 51 | room_id: int, 52 | ) -> MessageAdded: 53 | app = info.context["request"].app 54 | 55 | redis = await app["create_redis"]() 56 | 57 | res = await redis.subscribe(f"chat:{room_id}") 58 | ch = res[0] 59 | 60 | while await ch.wait_message(): 61 | data = await ch.get_json() 62 | yield MessageAdded( 63 | body=data["body"], 64 | id=data["id"], 65 | owner=User(id=data["user_id"], username=data["username"]), 66 | ) 67 | 68 | async def resolve_typing_start( 69 | self, 70 | info: ResolveInfo, 71 | room_id: int, 72 | ) -> MessageAdded: 73 | app = info.context["request"].app 74 | 75 | redis = await app["create_redis"]() 76 | 77 | res = await redis.subscribe(f"chat:typing:{room_id}") 78 | ch = res[0] 79 | 80 | while await ch.wait_message(): 81 | data = await ch.get_json() 82 | yield StartTyping(user=User(username=data["username"], id=data["id"])) 83 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/tests/test_messages.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | async def test_simple_fetch_messages_from_room(client, requests): 6 | executed_rooms = await client.execute( 7 | """{ 8 | rooms { 9 | id 10 | } 11 | } 12 | """, 13 | context_value=requests, 14 | ) 15 | 16 | single_room = executed_rooms["data"]["rooms"][0] 17 | room_id = single_room["id"] 18 | 19 | executed_room = await client.execute( 20 | """{ 21 | room(id: %s) { 22 | messages { 23 | id 24 | body 25 | favouriteCount 26 | owner { 27 | id 28 | email 29 | username 30 | } 31 | } 32 | } 33 | } 34 | """ 35 | % room_id, 36 | context_value=requests, 37 | ) 38 | 39 | messages = executed_room["data"]["room"]["messages"] 40 | errors = executed_room.get("errors", []) 41 | 42 | assert messages 43 | 44 | single_messages = messages[0] 45 | owner = single_messages["owner"] 46 | 47 | assert isinstance(single_messages["id"], int) 48 | assert not errors 49 | assert None not in owner.values() 50 | assert single_messages["body"] 51 | assert isinstance(owner["id"], int) 52 | assert owner["username"] 53 | assert owner["email"] 54 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/tests/test_messages_mutations.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | async def test_mutations_for_message(client, requests): 6 | text = 'test messages' 7 | room_id = 1 8 | owner_id = 1 9 | 10 | async def fetch_messages(room_id: int): 11 | return await client.execute( 12 | """{ 13 | room(id: %s) { 14 | messages { 15 | id 16 | body 17 | favouriteCount 18 | owner { 19 | id 20 | email 21 | username 22 | } 23 | } 24 | } 25 | } 26 | """ % room_id, 27 | context_value=requests, 28 | ) 29 | 30 | executed = await fetch_messages(room_id) 31 | messages = executed['data']['room']['messages'] 32 | init_last_messages = messages[-1] 33 | 34 | executed = await client.execute( 35 | """ 36 | mutation { 37 | addMessage(roomId: %s, ownerId: %s, body: "%s") { 38 | isCreated 39 | } 40 | } 41 | """ % (room_id, owner_id, text), 42 | context_value=requests, 43 | ) 44 | 45 | assert executed['data']['addMessage']['isCreated'] 46 | 47 | executed = await fetch_messages(room_id) 48 | messages = executed['data']['room']['messages'] 49 | last_messages = messages[-1] 50 | 51 | assert last_messages['owner']['id'] == owner_id 52 | assert last_messages['body'] == text 53 | 54 | executed = await client.execute( 55 | """ 56 | mutation { 57 | removeMessage(id: %s) { 58 | isRemoved 59 | } 60 | } 61 | """ % last_messages['id'], 62 | context_value=requests, 63 | ) 64 | 65 | assert executed['data']['removeMessage']['isRemoved'] 66 | 67 | executed = await fetch_messages(room_id) 68 | messages = executed['data']['room']['messages'] 69 | last_messages = messages[-1] 70 | 71 | assert last_messages == init_last_messages 72 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/tests/test_rooms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | async def test_simple_fetch_rooms(client, requests): 6 | executed = await client.execute( 7 | """{ 8 | rooms { 9 | id 10 | name 11 | } 12 | } 13 | """, 14 | context_value=requests, 15 | ) 16 | 17 | assert len(executed['data']['rooms']) == 1000 18 | 19 | single_room = executed['data']['rooms'][0] 20 | 21 | assert single_room['id'] == 1 22 | assert single_room['name'] == 'test#0' 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_fetch_data_connected_with_owner_of_room(client, requests): 27 | executed = await client.execute( 28 | """{ 29 | rooms { 30 | owner { 31 | id 32 | username 33 | email 34 | } 35 | } 36 | } 37 | """, 38 | context_value=requests, 39 | ) 40 | 41 | owner = executed['data']['rooms'][0]['owner'] 42 | 43 | assert owner['id'] 44 | assert owner['username'] 45 | assert owner['email'] 46 | 47 | 48 | @pytest.mark.asyncio 49 | async def test_fetch_single_room(client, requests): 50 | executed = await client.execute( 51 | """{ 52 | rooms { 53 | id 54 | name 55 | owner { 56 | id 57 | username 58 | email 59 | } 60 | } 61 | } 62 | """, 63 | context_value=requests, 64 | ) 65 | 66 | single_room = executed['data']['rooms'][0] 67 | room_id = single_room['id'] 68 | 69 | executed = await client.execute( 70 | """{ 71 | room(id: %s) { 72 | id 73 | name 74 | owner { 75 | id 76 | username 77 | email 78 | } 79 | } 80 | } 81 | """ % room_id, 82 | context_value=requests, 83 | ) 84 | 85 | assert executed['data']['room'] == single_room 86 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/tests/test_viewer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | async def test_viewer(client, requests): 6 | executed = await client.execute( 7 | """ 8 | { 9 | viewer { 10 | id 11 | } 12 | } 13 | """, 14 | return_value=requests, 15 | ) 16 | 17 | assert executed['data']['viewer'] is None 18 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/api/views.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import graphene 4 | from graphql.execution.executors.asyncio import AsyncioExecutor 5 | from aiohttp import web 6 | 7 | from graph.api.queries import Query 8 | from graph.api.mutations import Mutation 9 | from graph.api.subscriptions import Subscription 10 | from graph.api.contrib import ( 11 | CustomGraphQLView, 12 | CustomAiohttpSubscriptionServer, 13 | ) 14 | 15 | 16 | __all__ = ['GQL', 'subscriptions', ] 17 | 18 | 19 | schema = graphene.Schema( 20 | query=Query, 21 | mutation=Mutation, 22 | subscription=Subscription, 23 | ) 24 | subscription_server = CustomAiohttpSubscriptionServer(schema) 25 | 26 | 27 | def GQL(graphiql: bool = False) -> CustomGraphQLView: 28 | """The main view for give access to GraphQl. 29 | 30 | The view can work in two modes: 31 | 32 | - simple GraphQl handler 33 | - GraphIQL view for interactive work with graph application 34 | 35 | :param graphiql: bool 36 | :return: GraphQLView 37 | """ 38 | 39 | view = CustomGraphQLView( 40 | schema=schema, 41 | # TODO: make global loop 42 | executor=AsyncioExecutor(loop=asyncio.get_event_loop()), 43 | graphiql=graphiql, 44 | enable_async=True, 45 | # TODO: remove static url 46 | # socket="ws://localhost:8080/subscriptions" 47 | socket="ws://localhost:8080/subscriptions", 48 | ) 49 | return view 50 | 51 | 52 | async def subscriptions(request: web.Request) -> web.WebSocketResponse: 53 | """Handler for creating socket connection with apollo client and checking 54 | subscriptions. 55 | """ 56 | ws = web.WebSocketResponse(protocols=('graphql-ws',)) 57 | await ws.prepare(request) 58 | 59 | await subscription_server.handle( 60 | ws, 61 | request_context={"request": request} 62 | ) 63 | 64 | return ws 65 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/graphql-demo/graph/auth/__init__.py -------------------------------------------------------------------------------- /demos/graphql-demo/graph/auth/db_utils.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from graph.auth.models import User 4 | 5 | from sqlalchemy.sql import select 6 | 7 | 8 | async def select_users(session, keys: List[int]): 9 | result = await session.scalars( 10 | select(User).where(User.id.in_(keys)).order_by(User.id) 11 | ) 12 | return result.all() 13 | 14 | 15 | async def select_user(session, key: int): 16 | result = await session.scalars(select(User).where(User.id == key).order_by(User.id)) 17 | return result.first() 18 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/auth/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | 3 | 4 | @unique 5 | class UserGender(Enum): 6 | women = 'women' 7 | male = 'male' 8 | none = 'none' 9 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/auth/models.py: -------------------------------------------------------------------------------- 1 | from graph.auth.enums import UserGender 2 | from graph.db import Base 3 | from sqlalchemy import String 4 | from sqlalchemy.orm import Mapped, mapped_column 5 | 6 | 7 | class User(Base): 8 | __tablename__ = "users" 9 | 10 | id: Mapped[int] = mapped_column(primary_key=True, index=True) 11 | username: Mapped[str] = mapped_column(String(200), unique=True) 12 | email: Mapped[str] = mapped_column(String(200), unique=True) 13 | password: Mapped[str] = mapped_column(String(10)) 14 | avatar_url = Mapped[str] 15 | gender: Mapped[UserGender] = mapped_column(server_default=UserGender.none.value) 16 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/auth/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/graphql-demo/graph/auth/tests/__init__.py -------------------------------------------------------------------------------- /demos/graphql-demo/graph/auth/tests/test_auth_db_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from graph.auth.db_utils import select_user, select_users 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_select_user(db_sm): 8 | async with db_sm.begin() as sess: 9 | res = await select_user(sess, 1) 10 | 11 | assert res.id == 1 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_select_users(db_sm): 16 | async with db_sm.begin() as sess: 17 | res = await select_users(sess, [1, 2, 3]) 18 | 19 | assert isinstance(res, list) 20 | assert res[0].id == 1 21 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/graphql-demo/graph/chat/__init__.py -------------------------------------------------------------------------------- /demos/graphql-demo/graph/chat/db_utils.py: -------------------------------------------------------------------------------- 1 | from graph.chat.models import Message, Room 2 | from graph.constants import OBJECT_NOT_FOUND_ERROR 3 | from sqlalchemy.sql import select 4 | 5 | __all__ = [ 6 | "select_rooms", 7 | "select_messages_by_room_id", 8 | "select_room", 9 | "create_message", 10 | "delete_message", 11 | ] 12 | 13 | 14 | async def select_rooms(session) -> list[Room]: 15 | cursor = await session.scalars(select(Room).order_by(Room.id)) 16 | 17 | return cursor.all() 18 | 19 | 20 | async def select_room(session, id: int) -> Room: 21 | cursor = await session.scalars(select(Room).where(Room.id == id)) 22 | item = cursor.first() 23 | assert item, OBJECT_NOT_FOUND_ERROR 24 | 25 | return item 26 | 27 | 28 | async def select_messages_by_room_id(session, room_id: int) -> list[Message]: 29 | cursor = await session.scalars( 30 | select(Message).where(Message.room_id == room_id).order_by(Message.id) 31 | ) 32 | 33 | return cursor.all() 34 | 35 | 36 | async def create_message( 37 | session, 38 | room_id: int, 39 | owner_id: int, 40 | body: str, 41 | ) -> Message: 42 | new_msg = Message(body=body, owner_id=owner_id, room_id=room_id) 43 | session.add(new_msg) 44 | return new_msg 45 | 46 | 47 | async def delete_message(session, id: int): 48 | msg = await session.get(Message, id) 49 | await session.delete(msg) 50 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/chat/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from graph.auth.models import User 4 | from graph.db import Base 5 | from sqlalchemy import ARRAY, ForeignKey, Integer, String 6 | from sqlalchemy.orm import Mapped, mapped_column 7 | from sqlalchemy.sql import func 8 | 9 | 10 | class Room(Base): 11 | __tablename__ = "rooms" 12 | 13 | id: Mapped[int] = mapped_column(primary_key=True, index=True) 14 | name: Mapped[str] = mapped_column(String(20), unique=True) 15 | 16 | # ForeignKey 17 | owner_id: Mapped[int] = mapped_column(ForeignKey(User.id, ondelete="CASCADE")) 18 | 19 | 20 | class Message(Base): 21 | __tablename__ = "messages" 22 | 23 | id: Mapped[int] = mapped_column(primary_key=True, index=True) 24 | body: Mapped[str] 25 | created_at: Mapped[datetime] = mapped_column(server_default=func.now()) 26 | who_like: Mapped[list[int]] = mapped_column(ARRAY(Integer), server_default="{}") 27 | 28 | # ForeignKey 29 | owner_id: Mapped[int] = mapped_column(ForeignKey(User.id, ondelete="CASCADE")) 30 | room_id: Mapped[int] = mapped_column(ForeignKey(Room.id, ondelete="CASCADE")) 31 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/chat/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/graphql-demo/graph/chat/tests/__init__.py -------------------------------------------------------------------------------- /demos/graphql-demo/graph/chat/tests/test_chat_db_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from graph.chat.db_utils import ( 3 | create_message, 4 | delete_message, 5 | select_messages_by_room_id, 6 | select_room, 7 | select_rooms, 8 | ) 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_select_room(db_sm): 13 | async with db_sm() as conn: 14 | res = await select_room(conn, 1) 15 | 16 | assert res.id == 1 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_select_rooms(db_sm): 21 | async with db_sm() as conn: 22 | res = await select_rooms(conn) 23 | 24 | assert isinstance(res, list) 25 | assert res[0].id == 1 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_select_messages_by_room_id(db_sm): 30 | async with db_sm() as conn: 31 | res = await select_messages_by_room_id(conn, 1) 32 | 33 | assert isinstance(res, list) 34 | 35 | 36 | @pytest.mark.asyncio 37 | async def test_create_message(db_sm): 38 | async with db_sm() as conn: 39 | await create_message(conn, 1, 1, "Text") 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_delete_message(db_sm): 44 | async with db_sm() as conn: 45 | await delete_message(conn, 1) 46 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/config/api.test.yml: -------------------------------------------------------------------------------- 1 | postgres: 2 | user: test_postgres 3 | password: test_postgres 4 | database: test_postgres 5 | port: 5432 6 | host: localhost 7 | 8 | redis: 9 | port: 6379 10 | host: localhost 11 | 12 | app: 13 | host: 127.0.0.1 14 | port: 9000 15 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/config/api.yml: -------------------------------------------------------------------------------- 1 | postgres: 2 | user: postgres 3 | password: postgres 4 | database: postgres 5 | port: 5432 6 | host: localhost 7 | 8 | redis: 9 | port: 6379 10 | host: localhost 11 | 12 | app: 13 | host: 127.0.0.1 14 | port: 9000 15 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/constants.py: -------------------------------------------------------------------------------- 1 | OBJECT_NOT_FOUND_ERROR = 'Object not found' 2 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import DeclarativeBase 2 | 3 | 4 | class Base(DeclarativeBase): 5 | pass 6 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/graphql-demo/graph/main/__init__.py -------------------------------------------------------------------------------- /demos/graphql-demo/graph/main/views.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | import aiohttp_jinja2 3 | 4 | 5 | @aiohttp_jinja2.template('index.jinja2') 6 | async def index(request: web.Request) -> dict: 7 | return {} 8 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/routes.py: -------------------------------------------------------------------------------- 1 | from graph.utils import APP_PATH 2 | from graph.api.views import ( 3 | GQL, 4 | subscriptions, 5 | ) 6 | from graph.main.views import index 7 | 8 | 9 | def init_routes(app): 10 | add_route = app.router.add_route 11 | 12 | add_route('*', '/', index, name='index') 13 | add_route('*', '/graphql', GQL(), name='graphql') 14 | add_route('*', '/graphiql', GQL(graphiql=True), name='graphiql') 15 | add_route('*', '/subscriptions', subscriptions, name='subscriptions') 16 | 17 | # added static dir 18 | app.router.add_static( 19 | '/static/', 20 | path=(APP_PATH / 'static'), 21 | name='static', 22 | ) 23 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/templates/index.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | AioHttp graphQl example 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /demos/graphql-demo/graph/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import argparse 3 | 4 | from aiohttp import web 5 | from trafaret_config import commandline 6 | import trafaret 7 | 8 | # Paths 9 | APP_PATH = Path(__file__).parent 10 | DEFAULT_CONFIG_PATH = APP_PATH / 'config' / 'api.yml' 11 | 12 | 13 | CONFIG_TRAFARET = trafaret.Dict({ 14 | trafaret.Key('postgres'): 15 | trafaret.Dict({ 16 | 'user': trafaret.String(), 17 | 'password': trafaret.String(), 18 | 'database': trafaret.String(), 19 | 'host': trafaret.String(), 20 | 'port': trafaret.Int(), 21 | }), 22 | trafaret.Key('redis'): 23 | trafaret.Dict({ 24 | 'port': trafaret.Int(), 25 | 'host': trafaret.String(), 26 | }), 27 | trafaret.Key('app'): 28 | trafaret.Dict({ 29 | 'host': trafaret.IP, 30 | 'port': trafaret.Int(), 31 | }), 32 | }) 33 | 34 | 35 | def get_config(argv=None) -> dict: 36 | ap = argparse.ArgumentParser() 37 | commandline.standard_argparse_options( 38 | ap, 39 | default_config=DEFAULT_CONFIG_PATH, 40 | ) 41 | options = ap.parse_args(argv) 42 | 43 | return commandline.config_from_options(options, CONFIG_TRAFARET) 44 | 45 | 46 | def init_config(app: web.Application) -> None: 47 | app['config'] = get_config(['-c', DEFAULT_CONFIG_PATH.as_posix()]) 48 | -------------------------------------------------------------------------------- /demos/graphql-demo/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /demos/graphql-demo/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | # show 10 slowest invocations: 4 | --durations=10 5 | 6 | # a bit of verbosity doesn't hurt: 7 | -v 8 | 9 | # report all the things == -rxXs: 10 | -ra 11 | 12 | # show values of the local vars in errors: 13 | --showlocals 14 | asyncio_mode = auto 15 | filterwarnings = 16 | error 17 | testpaths = tests/ 18 | xfail_strict = true 19 | -------------------------------------------------------------------------------- /demos/graphql-demo/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.11.18 2 | aiohttp-jinja2==1.6 3 | aiohttp-graphql==1.1.0 4 | aiodataloader==0.4.2 5 | faker==37.3.0 6 | trafaret_config==2.0.2 7 | graphene==2.1.9 8 | graphql-core==2.3.2 9 | graphql-ws==0.4.4 10 | psycopg2-binary==2.9.10 11 | redis==6.1.0 12 | 13 | mypy==1.15.0 14 | flake8==7.2.0 15 | pytest==8.3.5 16 | pytest-cov==6.1.1 17 | sqlalchemy==2.0.41 18 | -------------------------------------------------------------------------------- /demos/graphql-demo/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | REGEXP = re.compile(r"^__version__\W*=\W*'([\d.abrc]+)'") 8 | 9 | 10 | def read_version(): 11 | 12 | init_py = os.path.join(os.path.dirname(__file__), 'graph', '__init__.py') 13 | 14 | with open(init_py) as f: 15 | for line in f: 16 | match = REGEXP.match(line) 17 | if match is not None: 18 | return match.group(1) 19 | msg = f'Cannot find version in ${init_py}' 20 | raise RuntimeError(msg) 21 | 22 | 23 | install_requires = [ 24 | 'aiohttp', 25 | 'aiohttp_jinja2', 26 | 'aiohttp_graphql', 27 | 'aiodataloader', 28 | 'trafaret_config', 29 | 'graphene==2.1.7', 30 | 'graphql-core==2.2.1', 31 | 'graphql-ws', 32 | 'psycopg2-binary', 33 | 'redis>=4.2', 34 | 'Faker', 35 | 'sqlalchemy', 36 | ] 37 | 38 | 39 | setup( 40 | name='graph', 41 | version=read_version(), 42 | description='The GraphQL example from aiohttp', 43 | platforms=['POSIX'], 44 | packages=find_packages(), 45 | package_data={ 46 | '': ['config/*.*'] 47 | }, 48 | include_package_data=True, 49 | install_requires=install_requires, 50 | zip_safe=False, 51 | ) 52 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | "stage-2" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-chat", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "apollo-client": "^1.3.0", 7 | "graphql": "^16.11.0", 8 | "graphql-tag": "^2.2.0", 9 | "immutability-helper": "^3.1.1", 10 | "react": "15.7.0", 11 | "react-apollo": "^1.2.0", 12 | "react-dom": "15.7.0", 13 | "recompose": "^0.30.0", 14 | "redux": "^3.6.0", 15 | "subscriptions-transport-ws": "^0.11.0" 16 | }, 17 | "devDependencies": { 18 | "babel-core": "^6.26.3", 19 | "babel-loader": "^7.1.4", 20 | "babel-preset-env": "^1.7.0", 21 | "babel-preset-react": "^6.24.1", 22 | "babel-preset-stage-2": "^6.24.1", 23 | "css-loader": "^7.1.2", 24 | "style-loader": "^4.0.0", 25 | "webpack": "^4.47.0", 26 | "webpack-cli": "^4.10.0" 27 | }, 28 | "scripts": { 29 | "start": "webpack --config ./webpack.config.babel.js --mode development --watch", 30 | "build": "webpack --config ./webpack.config.babel.js --mode production" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | } 6 | 7 | .content { 8 | flex: 1; 9 | } 10 | 11 | .main { 12 | display: flex; 13 | max-height: calc(100vh - 64px); 14 | overflow: hidden; 15 | } 16 | 17 | .bar { 18 | width: 300px; 19 | border-right: 1px solid rgba(0,0,0,0.14); 20 | flex: initial; 21 | max-height: calc(100vh - 64px); 22 | overflow: auto; 23 | } 24 | 25 | .chatroom:hover { 26 | background-color: #00A0FF; 27 | color: white; 28 | } 29 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AppProvider from './client/Provider'; 4 | import ChatRoom from './components/index'; 5 | import Header from './components/Header/Header'; 6 | import ChatMessages from './components/ChatMessages/ChatMessages'; 7 | 8 | // styles 9 | import './App.css'; 10 | 11 | 12 | export default class extends React.Component { 13 | state = { 14 | activeRoom: 1, 15 | }; 16 | 17 | changeRoom = (item) => { 18 | this.setState({activeRoom: item}); 19 | }; 20 | 21 | render () { 22 | return ( 23 | 24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 |
34 | ); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/client/Provider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ApolloProvider } from 'react-apollo'; 3 | 4 | import client from './'; 5 | 6 | 7 | export default function AppProvider({ children }) { 8 | return ( 9 | 10 |
11 | {children} 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/client/index.js: -------------------------------------------------------------------------------- 1 | import ApolloClient, { createNetworkInterface } from 'apollo-client'; 2 | import { 3 | SubscriptionClient, 4 | addGraphQLSubscriptions 5 | } from 'subscriptions-transport-ws'; 6 | 7 | const wsClient = new SubscriptionClient('ws://localhost:8080/subscriptions'); 8 | const baseNetworkInterface = createNetworkInterface({ 9 | uri: '/graphql', 10 | }); 11 | const subscriptionNetworkInterface = addGraphQLSubscriptions( 12 | baseNetworkInterface, 13 | wsClient, 14 | ); 15 | 16 | 17 | export default new ApolloClient({ 18 | networkInterface: subscriptionNetworkInterface, 19 | connectToDevTools: true, 20 | }); 21 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/components/ChatMessages/ChatMessages.css: -------------------------------------------------------------------------------- 1 | .message-block { 2 | max-height: calc(100vh - 64px - 50px); 3 | overflow: auto; 4 | flex: 1; 5 | } 6 | 7 | .message-wrapper-right { 8 | text-align: right; 9 | } 10 | 11 | .message-wrapper-left { 12 | text-align: left; 13 | } 14 | 15 | .message-username { 16 | padding-bottom: 5px; 17 | font-size: 11px; 18 | display: block; 19 | } 20 | 21 | .message { 22 | display: inline-block; 23 | background: rgba(0,0,0,0.1); 24 | padding: 5px; 25 | border-radius: 10px; 26 | max-width: 50%; 27 | border-bottom: 1px solid rgba(0,0,0,0.12); 28 | margin: 10px 20px; 29 | } 30 | 31 | .message p { 32 | font-size: 14px; 33 | letter-spacing: 2px; 34 | margin: 0; 35 | } 36 | 37 | .typing-block { 38 | margin: 10px 20px; 39 | font-size: 11px; 40 | color: #bbb; 41 | } 42 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/components/Header/Header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | height: 64px; 3 | background: #26a69a; 4 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2); 5 | } 6 | 7 | .header-title { 8 | color: #fff; 9 | line-height: 64px; 10 | padding-left: 20px; 11 | } 12 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // styles 4 | import './Header.css' 5 | 6 | 7 | export default () => ( 8 |
9 | GraphQl Messenger 10 |
11 | ) 12 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/components/MessageBox/MessageBox.css: -------------------------------------------------------------------------------- 1 | .message-box-wrapper { 2 | position: absolute; 3 | bottom: 0; 4 | height: 50px; 5 | width: calc(100% - 300px); 6 | overflow: hidden; 7 | border-top: 1px solid rgba(0,0,0,0.14); 8 | } 9 | 10 | .message-box { 11 | height: 50px; 12 | width: 100%; 13 | border: 0; 14 | padding: 16px 8px; 15 | font-size: 14px; 16 | box-sizing: border-box; 17 | outline: none; 18 | text-indent: 20px; 19 | } 20 | 21 | .message-box::placeholder { 22 | font-size: 14px; 23 | letter-spacing: 2px; 24 | } 25 | 26 | .button-wrapper { 27 | display: flex; 28 | justify-content: flex-end; 29 | position: absolute; 30 | bottom: 7px; 31 | right: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/components/Row/Row.css: -------------------------------------------------------------------------------- 1 | .chat-room { 2 | line-height: 44px; 3 | font-size: 13px; 4 | height: 44px; 5 | padding: 0 30px; 6 | color: rgba(0,0,0,0.87); 7 | display: block; 8 | cursor: pointer; 9 | transition: .3s ease-out; 10 | margin: 0; 11 | } 12 | 13 | .chat-room-active { 14 | background: rgba(0,0,0,0.1) !important; 15 | } 16 | 17 | .chat-room:hover { 18 | background-color: rgba(0,0,0,0.05); 19 | } -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/components/Row/Row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // styles 4 | import './Row.css' 5 | 6 | 7 | function ChatRoomRow({ 8 | closeMessages, 9 | onClick, 10 | active, 11 | id, 12 | title, 13 | }) { 14 | const className = active === id ? "chat-room chat-room-active": "chat-room"; 15 | 16 | return ( 17 |

18 | {title} 19 |

20 | ); 21 | } 22 | 23 | 24 | export default ChatRoomRow; 25 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/components/SendMessageButton/SendMessageButton.css: -------------------------------------------------------------------------------- 1 | .button-item { 2 | display: block; 3 | text-decoration: none; 4 | color: #fff; 5 | background-color: #26a69a; 6 | text-align: center; 7 | letter-spacing: 1px; 8 | transition: background-color .2s ease-out; 9 | cursor: pointer; 10 | font-size: 14px; 11 | outline: 0; 12 | border: none; 13 | border-radius: 2px; 14 | height: 36px; 15 | line-height: 36px; 16 | padding: 0 16px; 17 | text-transform: uppercase; 18 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2); 19 | } 20 | 21 | .button-item:hover { 22 | background-color: #1ba688; 23 | color: white; 24 | } 25 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/components/SendMessageButton/SendMessageButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // styles 4 | import './SendMessageButton.css' 5 | 6 | 7 | function SendMessageButton({sendMessage}) { 8 | return ( 9 | 12 | ); 13 | } 14 | 15 | export default SendMessageButton; 16 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/components/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import gql from 'graphql-tag'; 3 | import { graphql } from 'react-apollo'; 4 | import {compose, mapProps, withHandlers} from 'recompose'; 5 | import { withState } from 'recompose'; 6 | 7 | import ChatRoomRow from './Row/Row'; 8 | 9 | 10 | const chatRoomsQuery = gql` 11 | { 12 | rooms { 13 | id 14 | name 15 | } 16 | } 17 | `; 18 | 19 | 20 | function ChatRoom({chatRooms = [], openMessages, active}) { 21 | return ( 22 |
23 | {chatRooms.map(room => { 24 | return ( 25 | openMessages(room.id)} 31 | /> 32 | ); 33 | })} 34 |
35 | ); 36 | } 37 | 38 | export default compose( 39 | withState('active', 'setActive', 1), 40 | withHandlers({ 41 | openMessages: ({setActive, clickHandler}) => { 42 | return (id) => { 43 | clickHandler(id); 44 | 45 | return setActive(id); 46 | }; 47 | }}), 48 | graphql(chatRoomsQuery), 49 | mapProps(({data, ...rest}) => { 50 | const chatRooms = (data && data.rooms) || []; 51 | return { 52 | chatRooms, 53 | ...rest, 54 | }; 55 | }) 56 | )(ChatRoom); 57 | 58 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Lato'); 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | } 8 | 9 | * { 10 | font-family: 'Lato', sans-serif; 11 | } 12 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | // styles 7 | import './index.css'; 8 | 9 | 10 | ReactDOM.render( 11 | , 12 | document.getElementById('app') 13 | ); 14 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/src/utils.js: -------------------------------------------------------------------------------- 1 | import { random } from 'lodash'; 2 | 3 | export function getUser() { 4 | let user_id = window.localStorage.getItem('id'); 5 | 6 | if (!user_id) { 7 | user_id = random(1, 10); 8 | window.localStorage.setItem('id', user_id); 9 | } else { 10 | user_id = Number(user_id) 11 | } 12 | 13 | return user_id 14 | } 15 | -------------------------------------------------------------------------------- /demos/graphql-demo/ui/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | 4 | const STATIC_FOLDER = path.join(__dirname, '..', '/graph/static'); 5 | 6 | 7 | const config = { 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.(js|jsx)$/, 12 | exclude: /node_modules/, 13 | use: ['babel-loader'] 14 | }, { 15 | test: /\.(css)$/, 16 | use: [ 17 | { loader: "style-loader" }, 18 | { loader: "css-loader" } 19 | ] 20 | } 21 | ] 22 | }, 23 | resolve: { 24 | extensions: ['*', '.js', '.jsx'] 25 | }, 26 | output: { 27 | path: STATIC_FOLDER, 28 | publicPath: '/', 29 | filename: 'bundle.js' 30 | } 31 | }; 32 | 33 | export default config; 34 | -------------------------------------------------------------------------------- /demos/imagetagger/Makefile: -------------------------------------------------------------------------------- 1 | # Some simple testing tasks (sorry, UNIX only). 2 | 3 | PROJECT_NAME=imagetagger 4 | 5 | flake: 6 | flake8 $(PROJECT_NAME) setup.py 7 | 8 | test: 9 | py.test -s -v ./tests/ 10 | 11 | checkrst: 12 | python setup.py check --restructuredtext 13 | 14 | cov cover coverage: flake checkrst 15 | py.test -s -v --cov-report term --cov-report html --cov $(PROJECT_NAME) ./tests 16 | @echo "open file://`pwd`/htmlcov/index.html" 17 | 18 | clean: 19 | rm -rf `find . -name __pycache__` 20 | find . -type f -name '*.py[co]' -delete 21 | find . -type f -name '*~' -delete 22 | find . -type f -name '.*~' -delete 23 | find . -type f -name '@*' -delete 24 | find . -type f -name '#*#' -delete 25 | find . -type f -name '*.orig' -delete 26 | find . -type f -name '*.rej' -delete 27 | rm -f .coverage 28 | rm -rf coverage 29 | rm -rf build 30 | rm -rf htmlcov 31 | rm -rf dist 32 | 33 | run: 34 | python -m $(PROJECT_NAME) 35 | 36 | 37 | .PHONY: flake clean 38 | -------------------------------------------------------------------------------- /demos/imagetagger/README.rst: -------------------------------------------------------------------------------- 1 | Imagetagger Keras Demo 2 | ====================== 3 | 4 | Simple application how to use aiohttp_ for deep learning project with keras_. 5 | 6 | **Imagetagger** is API for image recognition, employs mobilenet_ network to 7 | classify images, but other supported network can be used. 8 | 9 | 10 | Install the app:: 11 | 12 | $ git clone https://github.com/aio-libs/aiohttp-demos.git 13 | $ cd demos/imagetagger/ 14 | $ pip install -r requirements/development.txt 15 | 16 | 17 | Run application:: 18 | 19 | $ make run 20 | 21 | 22 | Example of request:: 23 | 24 | curl -F file=@tests/data/aircraft.jpg http://localhost:8000/predict 25 | 26 | Example of response:: 27 | 28 | {"predictions": [ 29 | {"label": "envelope", "probability": 0.3094555735588074}, 30 | {"label": "airship", "probability": 0.20662210881710052}, 31 | {"label": "carton", "probability": 0.07820204645395279}, 32 | {"label": "freight_car", "probability": 0.056770652532577515}, 33 | {"label": "airliner", "probability": 0.04821280017495155}], 34 | "success": true} 35 | 36 | 37 | Requirements 38 | ============ 39 | * Python3.6 40 | * aiohttp_ 41 | * keras_ 42 | 43 | 44 | .. _Python: https://www.python.org 45 | .. _aiohttp: https://github.com/aio-libs/aiohttp 46 | .. _keras: https://keras.io/ 47 | .. _mobilenet: https://keras.io/applications/#mobilenet 48 | -------------------------------------------------------------------------------- /demos/imagetagger/config/api.dev.yml: -------------------------------------------------------------------------------- 1 | app: 2 | host: 127.0.0.1 3 | port: 8000 4 | workers: 5 | max_workers: 1 6 | model_path: "tests/data/mobilenet.h5" 7 | -------------------------------------------------------------------------------- /demos/imagetagger/config/api.prod.yml: -------------------------------------------------------------------------------- 1 | app: 2 | host: 0.0.0.0 3 | port: 8080 4 | 5 | workers: 6 | max_workers: 1 7 | model_path: "tests/data/mobilenet.h5" 8 | -------------------------------------------------------------------------------- /demos/imagetagger/config/api.test.yml: -------------------------------------------------------------------------------- 1 | app: 2 | host: 0.0.0.0 3 | port: 9000 4 | 5 | workers: 6 | max_workers: 1 7 | model_path: "tests/data/mobilenet.h5" 8 | -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.1' 2 | -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/__main__.py: -------------------------------------------------------------------------------- 1 | from .app import main 2 | 3 | 4 | if __name__ == '__main__': 5 | main() 6 | -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | from typing import Any 4 | 5 | import aiohttp_jinja2 6 | import jinja2 7 | from aiohttp import web 8 | 9 | from .routes import init_routes 10 | from .utils import Config, get_config, init_workers 11 | from .views import SiteHandler 12 | 13 | 14 | path = Path(__file__).parent 15 | 16 | 17 | def init_jinja2(app: web.Application) -> None: 18 | aiohttp_jinja2.setup( 19 | app, 20 | loader=jinja2.FileSystemLoader(str(path / 'templates')) 21 | ) 22 | 23 | 24 | async def init_app(conf: Config) -> web.Application: 25 | app = web.Application() 26 | executor = await init_workers(app, conf.workers) 27 | init_jinja2(app) 28 | handler = SiteHandler(conf, executor) 29 | init_routes(app, handler) 30 | return app 31 | 32 | 33 | async def get_app(): 34 | """Used by aiohttp-devtools for local development.""" 35 | import aiohttp_debugtoolbar 36 | app = await init_app(get_config()) 37 | aiohttp_debugtoolbar.setup(app) 38 | return app 39 | 40 | 41 | def main(args: Any = None) -> None: 42 | conf = get_config(args) 43 | loop = asyncio.get_event_loop() 44 | app = loop.run_until_complete(init_app(conf)) 45 | web.run_app(app, host=conf.app.host, port=conf.app.port) 46 | -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/constants.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | 4 | OBJECT_NOT_FOUND_ERROR = 'Object not found' 5 | PROJECT_DIR = pathlib.Path(__file__).parent.parent 6 | -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/routes.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from aiohttp import web 4 | 5 | from .views import SiteHandler 6 | 7 | PROJECT_PATH = pathlib.Path(__file__).parent 8 | 9 | 10 | def init_routes(app: web.Application, handler: SiteHandler) -> None: 11 | add_route = app.router.add_route 12 | 13 | add_route('GET', '/', handler.index, name='index') 14 | add_route('POST', '/predict', handler.predict, name='predict') 15 | 16 | # added static dir 17 | app.router.add_static( 18 | '/static/', path=(PROJECT_PATH / 'static'), name='static' 19 | ) 20 | -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/imagetagger/imagetagger/static/images/favicon.ico -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/imagetagger/imagetagger/static/images/logo.png -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/static/styles/main.css: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | body { 4 | min-height: 100vh; 5 | margin: 0; 6 | padding: 0; 7 | font-family: Helvetica , sans-serif; 8 | } 9 | 10 | /* header */ 11 | 12 | .header { 13 | padding: 35px 0; 14 | color: #fff; 15 | text-align: center; 16 | background: #20232a; 17 | } 18 | 19 | .banner-logo { 20 | display: block; 21 | margin: auto; 22 | width: 70px; 23 | } 24 | 25 | .banner-title { 26 | margin-bottom: 0; 27 | margin-top: 20px; 28 | font-size: 50px; 29 | } 30 | 31 | /* content */ 32 | 33 | main { 34 | flex: 1; 35 | max-width: 1000px; 36 | width: 100%; 37 | margin: auto; 38 | color: black; 39 | text-align: center; 40 | } 41 | 42 | .subtitle { 43 | font-size: 24px; 44 | text-align: center; 45 | } 46 | 47 | .wrapper { 48 | max-width: 800px; 49 | margin: auto; 50 | padding: 20px; 51 | } 52 | 53 | .image-block { 54 | margin-top: 40px; 55 | text-align: left; 56 | display: flex; 57 | } 58 | 59 | .image-wrapper, .predict-info { 60 | width: 50%; 61 | } 62 | 63 | .predict-info { 64 | margin-left: 20px; 65 | } 66 | 67 | #image-preview { 68 | width: 100%; 69 | } 70 | 71 | /* github link */ 72 | 73 | .fork-me-on-github { 74 | position: absolute; 75 | top: 0; 76 | right: 0; 77 | } 78 | 79 | /* common */ 80 | .btn { 81 | display: inline-block; 82 | padding: 7px 16px; 83 | margin-bottom: 0; 84 | font-size: 14px; 85 | font-weight: 400; 86 | line-height: 1.42857143; 87 | text-align: center; 88 | white-space: nowrap; 89 | vertical-align: middle; 90 | -ms-touch-action: manipulation; 91 | touch-action: manipulation; 92 | cursor: pointer; 93 | -webkit-user-select: none; 94 | -moz-user-select: none; 95 | -ms-user-select: none; 96 | user-select: none; 97 | background-image: none; 98 | border: 1px solid transparent; 99 | border-radius: 4px; 100 | outline: none; 101 | transition: background-color .2s; 102 | } 103 | 104 | .btn-success { 105 | color: #fff; 106 | background-color: #5cb85c; 107 | border-color: #4cae4c; 108 | } 109 | 110 | .btn-success:hover { 111 | background-color: #1eb846; 112 | } -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/types.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/imagetagger/imagetagger/types.py -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/views.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Dict 3 | from concurrent.futures import ProcessPoolExecutor 4 | 5 | import aiohttp_jinja2 6 | from aiohttp import web 7 | 8 | from .worker import predict 9 | from .utils import Config, executor_key 10 | 11 | 12 | class SiteHandler: 13 | def __init__(self, conf: Config, executor: ProcessPoolExecutor) -> None: 14 | self._conf = conf 15 | self._executor = executor 16 | self._loop = asyncio.get_event_loop() 17 | 18 | @aiohttp_jinja2.template('index.html') 19 | async def index(self, request: web.Request) -> Dict[str, str]: 20 | return {} 21 | 22 | async def predict(self, request: web.Request) -> web.Response: 23 | form = await request.post() 24 | raw_data = form['file'].file.read() 25 | form["file"].file.close() # Not needed in aiohttp 4+. 26 | executor = request.app[executor_key] 27 | r = self._loop.run_in_executor 28 | raw_data = await r(executor, predict, raw_data) 29 | # raw_data = predict(raw_data) 30 | headers = {'Content-Type': 'application/json'} 31 | return web.Response(body=raw_data, headers=headers) 32 | -------------------------------------------------------------------------------- /demos/imagetagger/imagetagger/worker.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import signal 4 | import numpy as np 5 | from typing import Tuple, Dict, Any, Optional 6 | 7 | from keras.models import load_model 8 | from keras.applications import imagenet_utils 9 | from tensorflow.keras.preprocessing.image import img_to_array 10 | from PIL import Image 11 | 12 | 13 | _model = None 14 | 15 | 16 | def warm(model_path: str) -> None: 17 | # should be executed only in child processes 18 | signal.signal(signal.SIGINT, signal.SIG_IGN) 19 | global _model 20 | if _model is None: 21 | _model = load_model(model_path) 22 | 23 | 24 | def clean() -> None: 25 | # should be executed only in child processes 26 | signal.signal(signal.SIGINT, signal.SIG_DFL) 27 | global _model 28 | _model = None 29 | 30 | 31 | def prepare_image(image: Image, target: Tuple[int, int]) -> Image: 32 | if image.mode != 'RGB': 33 | image = image.convert('RGB') 34 | 35 | image = image.resize(target) 36 | image = img_to_array(image) 37 | image = np.expand_dims(image, axis=0) 38 | image = imagenet_utils.preprocess_input(image) 39 | return image 40 | 41 | 42 | def predict(raw_data: bytes, model: Optional[Any] = None) -> bytes: 43 | if model is None: 44 | model = _model 45 | 46 | if model is None: 47 | raise RuntimeError('Model should be loaded first') 48 | 49 | data: Dict[str, Any] = {} 50 | with io.BytesIO(raw_data) as b_io: 51 | with Image.open(b_io) as f: 52 | image = prepare_image(f, target=(224, 224)) 53 | 54 | preds = model.predict(image) 55 | results = imagenet_utils.decode_predictions(preds) 56 | 57 | # loop over the results and add them to the list of 58 | # returned predictions 59 | data['predictions'] = [{'label': label, 'probability': float(prob)} 60 | for _, label, prob in results[0]] 61 | 62 | data['success'] = True 63 | return json.dumps(data).encode('utf-8') 64 | -------------------------------------------------------------------------------- /demos/imagetagger/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | # show 10 slowest invocations: 4 | --durations=10 5 | 6 | # a bit of verbosity doesn't hurt: 7 | -v 8 | 9 | # report all the things == -rxXs: 10 | -ra 11 | 12 | # show values of the local vars in errors: 13 | --showlocals 14 | asyncio_mode = auto 15 | filterwarnings = 16 | error 17 | testpaths = tests/ 18 | xfail_strict = true 19 | -------------------------------------------------------------------------------- /demos/imagetagger/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.11.18 2 | aiohttp-jinja2==1.6 3 | keras==3.10.0 4 | Pillow==11.2.1 5 | tensorflow==2.18.1 6 | trafaret_config==2.0.2 7 | -------------------------------------------------------------------------------- /demos/imagetagger/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | REGEXP = re.compile(r"^__version__\W*=\W*'([\d.abrc]+)'") 8 | 9 | 10 | def read_version(): 11 | 12 | init_py = os.path.join( 13 | os.path.dirname(__file__), 'imagetagger', '__init__.py' 14 | ) 15 | 16 | with open(init_py) as f: 17 | for line in f: 18 | match = REGEXP.match(line) 19 | if match is not None: 20 | return match.group(1) 21 | msg = f'Cannot find version in ${init_py}' 22 | raise RuntimeError(msg) 23 | 24 | 25 | install_requires = ['aiohttp', 'aiohttp_jinja2', 'trafaret_config'] 26 | 27 | 28 | setup( 29 | name='imagetagger', 30 | version=read_version(), 31 | description='imagetagger', 32 | platforms=['POSIX'], 33 | packages=find_packages(), 34 | package_data={'': ['config/*.*']}, 35 | include_package_data=True, 36 | install_requires=install_requires, 37 | zip_safe=False, 38 | ) 39 | -------------------------------------------------------------------------------- /demos/imagetagger/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/imagetagger/tests/__init__.py -------------------------------------------------------------------------------- /demos/imagetagger/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from imagetagger.utils import config_from_dict 4 | from imagetagger.app import init_app 5 | 6 | 7 | @pytest.fixture 8 | def loop(event_loop): 9 | return event_loop 10 | 11 | 12 | @pytest.fixture(scope='session') 13 | def conf(): 14 | d = { 15 | 'app': {'host': 'localhost', 'port': 9100}, 16 | 'workers': {'max_workers': 1, 17 | 'model_path': 'tests/data/mobilenet.h5'}, 18 | } 19 | return config_from_dict(d) 20 | 21 | 22 | @pytest.fixture 23 | def api(loop, aiohttp_client, conf): 24 | app = loop.run_until_complete(init_app(conf)) 25 | yield loop.run_until_complete(aiohttp_client(app)) 26 | loop.run_until_complete(app.shutdown()) 27 | -------------------------------------------------------------------------------- /demos/imagetagger/tests/data/aircraft.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/imagetagger/tests/data/aircraft.jpg -------------------------------------------------------------------------------- /demos/imagetagger/tests/data/hotdog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/imagetagger/tests/data/hotdog.jpg -------------------------------------------------------------------------------- /demos/imagetagger/tests/data/mobilenet.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/imagetagger/tests/data/mobilenet.h5 -------------------------------------------------------------------------------- /demos/imagetagger/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from aiohttp import FormData 4 | 5 | DATA_PATH = Path(__file__).parent / "data" 6 | 7 | 8 | async def test_index(api): 9 | resp = await api.get('/') 10 | 11 | assert resp.status == 200 12 | assert 'Imagetagger' in await resp.text() 13 | 14 | 15 | async def test_predict(api): 16 | hotdog_path = DATA_PATH / "hotdog.jpg" 17 | img = hotdog_path.read_bytes() 18 | data = FormData() 19 | data.add_field( 20 | 'file', img, filename='aircraft.jpg', content_type='image/img') 21 | 22 | resp = await api.post('/predict', data=data) 23 | assert resp.status == 200, resp 24 | data = await resp.json() 25 | assert data['success'] 26 | assert data['predictions'][0]['label'] 27 | -------------------------------------------------------------------------------- /demos/moderator/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": ["last 2 versions", "safari >= 7"] 8 | }, 9 | "modules": false 10 | } 11 | ], 12 | "preact" 13 | ], 14 | "env": { 15 | "test": { 16 | "presets": [ 17 | [ 18 | "env", 19 | { 20 | "targets": { 21 | "browsers": ["last 2 versions", "safari >= 7"] 22 | }, 23 | "modules": "commonjs" 24 | } 25 | ], 26 | "preact", 27 | "jest" 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /demos/moderator/.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /demos/moderator/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true 5 | }, 6 | "extends": [ 7 | "airbnb-base", 8 | "prettier" 9 | ], 10 | "plugins": [ 11 | "json", 12 | "react", 13 | "prettier" 14 | ], 15 | "rules": { 16 | "no-unused-vars": [0, { "varsIgnorePattern": "^h$" }], 17 | "prettier/prettier": "error" 18 | }, 19 | "parserOptions": { 20 | "ecmaFeatures": { 21 | "jsx": true, 22 | "modules": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demos/moderator/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "modules": true, 3 | "plugins": { 4 | "autoprefixer": { 5 | "grid": true 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /demos/moderator/Makefile: -------------------------------------------------------------------------------- 1 | # Some simple testing tasks (sorry, UNIX only). 2 | 3 | FLAGS= 4 | 5 | build: 6 | yarn run build 7 | 8 | flake: 9 | flake8 moderator model setup.py 10 | 11 | test: 12 | py.test -s -v $(FLAGS) ./tests/ 13 | 14 | checkrst: 15 | python setup.py check --restructuredtext 16 | 17 | cov cover coverage: flake checkrst 18 | py.test -s -v --cov-report term --cov-report html --cov moderator ./tests 19 | @echo "open file://`pwd`/htmlcov/index.html" 20 | 21 | clean: 22 | rm -rf `find . -name __pycache__` 23 | find . -type f -name '*.py[co]' -delete 24 | find . -type f -name '*~' -delete 25 | find . -type f -name '.*~' -delete 26 | find . -type f -name '@*' -delete 27 | find . -type f -name '#*#' -delete 28 | find . -type f -name '*.orig' -delete 29 | find . -type f -name '*.rej' -delete 30 | rm -f .coverage 31 | rm -rf coverage 32 | rm -rf build 33 | rm -rf htmlcov 34 | rm -rf dist 35 | 36 | run: 37 | python -m moderator 38 | 39 | 40 | .PHONY: flake clean 41 | -------------------------------------------------------------------------------- /demos/moderator/README.rst: -------------------------------------------------------------------------------- 1 | Moderator AI Demo 2 | ================= 3 | .. image:: https://travis-ci.org/jettify/moderator.ai.svg?branch=master 4 | :target: https://travis-ci.org/jettify/moderator.ai 5 | 6 | 7 | .. image:: https://raw.githubusercontent.com/jettify/moderator.ai/master/docs/preview.png 8 | 9 | Simple application how to use aiohttp_ for machine learning project. Project is 10 | API and UI for classification of toxic and offensive comments, based on data 11 | from kaggle_ competition. Project can be used as example how to separate CPU 12 | intensive tasks from blocking event loop. 13 | 14 | 15 | Install the app:: 16 | 17 | $ cd moderator 18 | $ pip install -r requirements-dev.txt 19 | 20 | 21 | Run application:: 22 | 23 | $ make run 24 | 25 | Open browser:: 26 | 27 | http://127.0.0.1:9001 28 | 29 | 30 | Requirements 31 | ============ 32 | * aiohttp_ 33 | 34 | 35 | .. _Python: https://www.python.org 36 | .. _aiohttp: https://github.com/aio-libs/aiohttp 37 | .. _kaggle: https://www.kaggle.com/c/jigsaw-toxic-comment-classification-challenge 38 | -------------------------------------------------------------------------------- /demos/moderator/config/config.yml: -------------------------------------------------------------------------------- 1 | host: 127.0.0.1 2 | port: 9001 3 | 4 | max_workers: 1 5 | model_path: "model/pipeline.dat" 6 | -------------------------------------------------------------------------------- /demos/moderator/docs/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/moderator/docs/preview.png -------------------------------------------------------------------------------- /demos/moderator/model/pipeline.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/moderator/model/pipeline.dat -------------------------------------------------------------------------------- /demos/moderator/moderator/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.1' 2 | -------------------------------------------------------------------------------- /demos/moderator/moderator/__main__.py: -------------------------------------------------------------------------------- 1 | from moderator.main import main 2 | 3 | 4 | if __name__ == '__main__': 5 | main() 6 | -------------------------------------------------------------------------------- /demos/moderator/moderator/build_model.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | from moderator.consts import PROJ_ROOT 5 | from moderator.model.pipeline import build_model 6 | 7 | 8 | if __name__ == '__main__': 9 | dataset_path = Path(sys.argv[1]) 10 | model_path = PROJ_ROOT / 'model' 11 | build_model(dataset_path, model_path) 12 | -------------------------------------------------------------------------------- /demos/moderator/moderator/consts.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | 4 | PROJ_ROOT = pathlib.Path(__file__).parent.parent 5 | -------------------------------------------------------------------------------- /demos/moderator/moderator/exceptions.py: -------------------------------------------------------------------------------- 1 | import json 2 | from aiohttp import web 3 | 4 | 5 | class AdminRESTError(web.HTTPError): 6 | status_code = 500 7 | error = 'Unknown Error' 8 | 9 | def __init__(self, message=None, status_code=None, **kwargs): 10 | 11 | if status_code is not None: 12 | self.status_code = status_code 13 | 14 | super().__init__(reason=message) 15 | if not message: 16 | message = self.error 17 | 18 | msg_dict = {'error': message} 19 | 20 | if kwargs: 21 | msg_dict['error_details'] = kwargs 22 | 23 | self.text = json.dumps(msg_dict) 24 | self.content_type = 'application/json' 25 | 26 | 27 | class ObjectNotFound(AdminRESTError): 28 | status_code = 404 29 | error = 'Object not found' 30 | 31 | 32 | class JsonValidaitonError(AdminRESTError): 33 | status_code = 400 34 | error = 'Invalid json payload' 35 | -------------------------------------------------------------------------------- /demos/moderator/moderator/handlers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiohttp import web 3 | 4 | from .worker import predict_probability 5 | from .utils import CommentList, ModerateList, validate_payload 6 | 7 | 8 | class SiteHandler: 9 | 10 | def __init__(self, conf, executor, project_root): 11 | self._conf = conf 12 | self._executor = executor 13 | self._root = project_root 14 | self._loop = asyncio.get_event_loop() 15 | 16 | async def index(self, request): 17 | path = str(self._root / 'static' / 'index.html') 18 | return web.FileResponse(path) 19 | 20 | async def moderate(self, request): 21 | raw_data = await request.read() 22 | data = validate_payload(raw_data, CommentList) 23 | 24 | features = [d['comment'] for d in data] 25 | run = self._loop.run_in_executor 26 | results = await run(self._executor, predict_probability, features) 27 | payload = ModerateList([{ 28 | 'toxic': f'{r[0]:.2f}', 29 | 'severe_toxic': f'{r[1]:.2f}', 30 | 'obscene': f'{r[2]:.2f}', 31 | 'insult': f'{r[3]:.2f}', 32 | 'identity_hate': f'{r[4]:.2f}' 33 | } for r in results]) 34 | return web.json_response(payload) 35 | -------------------------------------------------------------------------------- /demos/moderator/moderator/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from aiohttp import web 5 | 6 | from moderator.consts import PROJ_ROOT 7 | from moderator.handlers import SiteHandler 8 | from moderator.routes import setup_routes 9 | from moderator.utils import load_config, setup_executor 10 | 11 | 12 | async def init(conf): 13 | app = web.Application() 14 | executor = await setup_executor(app, conf) 15 | handler = SiteHandler(conf, executor, PROJ_ROOT) 16 | setup_routes(app, handler, PROJ_ROOT) 17 | return app 18 | 19 | 20 | async def get_app(): 21 | """Used by aiohttp-devtools for local development.""" 22 | import aiohttp_debugtoolbar 23 | conf = load_config(PROJ_ROOT / 'config' / 'config.yml') 24 | app = await init(conf) 25 | aiohttp_debugtoolbar.setup(app) 26 | return app 27 | 28 | 29 | def main(): 30 | logging.basicConfig(level=logging.DEBUG) 31 | 32 | loop = asyncio.get_event_loop() 33 | conf = load_config(PROJ_ROOT / 'config' / 'config.yml') 34 | app = loop.run_until_complete(init(conf)) 35 | host, port = conf['host'], conf['port'] 36 | web.run_app(app, host=host, port=port) 37 | 38 | 39 | if __name__ == '__main__': 40 | main() 41 | -------------------------------------------------------------------------------- /demos/moderator/moderator/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/moderator/moderator/model/__init__.py -------------------------------------------------------------------------------- /demos/moderator/moderator/model/pipeline.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from pathlib import Path 3 | 4 | import pandas as pd 5 | 6 | from sklearn.feature_extraction.text import TfidfVectorizer 7 | from sklearn.linear_model import LogisticRegression 8 | from sklearn.multioutput import MultiOutputClassifier 9 | from sklearn.pipeline import Pipeline 10 | 11 | 12 | def read_data(dataset_path): 13 | class_names = ['toxic', 'severe_toxic', 'obscene', 14 | 'insult', 'identity_hate'] 15 | train = pd.read_csv(dataset_path).fillna(' ') 16 | train_text = train['comment_text'] 17 | train_targets = train[class_names] 18 | return train_text, train_targets 19 | 20 | 21 | def build_pipeline(): 22 | seed = 1234 23 | word_vectorizer = TfidfVectorizer( 24 | sublinear_tf=True, 25 | strip_accents='unicode', 26 | analyzer='word', 27 | token_pattern=r'\w{1,}', 28 | stop_words='english', 29 | ngram_range=(1, 1), 30 | max_features=10000, 31 | ) 32 | 33 | logistic = LogisticRegression(C=0.1, solver='sag', random_state=seed) 34 | classifier = MultiOutputClassifier(logistic) 35 | 36 | pipeline = Pipeline(steps=[ 37 | ('word_tfidf', word_vectorizer), 38 | ('logistic', classifier) 39 | ]) 40 | return pipeline 41 | 42 | 43 | def build_model(dataset_path: Path, model_path: Path) -> None: 44 | train, targets = read_data(dataset_path) 45 | 46 | pipeline = build_pipeline() 47 | pipeline.fit(train, targets) 48 | 49 | output_path = model_path / "pipeline.dat" 50 | with output_path.open("wb") as f: 51 | pickle.dump(pipeline, f) 52 | -------------------------------------------------------------------------------- /demos/moderator/moderator/routes.py: -------------------------------------------------------------------------------- 1 | def setup_routes(app, handler, project_root): 2 | router = app.router 3 | h = handler 4 | router.add_get('/', h.index, name='index') 5 | router.add_post('/moderate', h.moderate, name='moderate') 6 | router.add_static( 7 | '/static/', path=str(project_root / 'static'), 8 | name='static') 9 | -------------------------------------------------------------------------------- /demos/moderator/moderator/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from concurrent.futures import ProcessPoolExecutor 4 | 5 | import trafaret as t 6 | import yaml 7 | 8 | from .consts import PROJ_ROOT 9 | from .exceptions import JsonValidaitonError 10 | from .worker import warm 11 | 12 | 13 | def load_config(fname): 14 | with open(fname, 'rt') as f: 15 | data = yaml.safe_load(f) 16 | # TODO: add config validation 17 | return data 18 | 19 | 20 | Comment = t.Dict({ 21 | t.Key('comment'): t.String, 22 | }) 23 | CommentList = t.List(Comment, max_length=10) 24 | 25 | 26 | ModerateView = t.Dict({ 27 | t.Key('toxic'): t.Float[0:1], 28 | t.Key('severe_toxic'): t.Float[0:1], 29 | t.Key('obscene'): t.Float[0:1], 30 | t.Key('insult'): t.Float[0:1], 31 | t.Key('identity_hate'): t.Float[0:1], 32 | }) 33 | ModerateList = t.List(ModerateView) 34 | 35 | 36 | def validate_payload(raw_payload, schema): 37 | payload = raw_payload.decode(encoding='UTF-8') 38 | try: 39 | parsed = json.loads(payload) 40 | except ValueError: 41 | raise JsonValidaitonError('Payload is not json serialisable') 42 | 43 | try: 44 | data = schema(parsed) 45 | except t.DataError as exc: 46 | result = exc.as_dict() 47 | raise JsonValidaitonError(result) 48 | return data 49 | 50 | 51 | async def setup_executor(app, conf): 52 | n = conf['max_workers'] 53 | 54 | executor = ProcessPoolExecutor(max_workers=n) 55 | path = str(PROJ_ROOT / conf['model_path']) 56 | loop = asyncio.get_event_loop() 57 | run = loop.run_in_executor 58 | fs = [run(executor, warm, path) for i in range(0, n)] 59 | await asyncio.gather(*fs) 60 | 61 | async def close_executor(app): 62 | # TODO: figureout timeout for shutdown 63 | executor.shutdown(wait=True) 64 | 65 | app.on_cleanup.append(close_executor) 66 | return executor 67 | -------------------------------------------------------------------------------- /demos/moderator/moderator/worker.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import numpy as np 3 | 4 | _model = None 5 | 6 | 7 | def warm(model_path): 8 | global _model 9 | if _model is None: 10 | with open(model_path, 'rb') as f: 11 | pipeline = pickle.load(f) 12 | _model = pipeline 13 | return True 14 | 15 | 16 | def predict_probability(comments): 17 | results = _model.predict_proba(np.array(comments)) 18 | return np.array(results).T[1].tolist() 19 | -------------------------------------------------------------------------------- /demos/moderator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toxic-comments-client", 3 | "version": "0.0.1", 4 | "engines": { 5 | "node": ">=6" 6 | }, 7 | "scripts": { 8 | "build": "./node_modules/.bin/parcel build --out-dir static ui/index.html --public-url ./static/", 9 | "dev": "./node_modules/.bin/parcel ui/index.html", 10 | "prettier": "prettier --write ui/**/* test/**/*", 11 | "eslint": "eslint ui/**/*.js test/**/*.js", 12 | "eslint-check": "eslint --print-config .eslintrc.json | eslint-config-prettier-check", 13 | "pretest": "npm-run-all eslint-check eslint", 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "snyk-protect": "snyk protect", 16 | "prepublish": "yarn run snyk-protect" 17 | }, 18 | "license": "MIT", 19 | "dependencies": { 20 | "npm": "^11.4.1", 21 | "package.json": "^2.0.1", 22 | "parcel": "^2.15.2", 23 | "preact": "10.26.7", 24 | "snyk": "^1.1297.1", 25 | "whatwg-fetch": "3.6.20", 26 | "yarn": "^1.22.22" 27 | }, 28 | "devDependencies": { 29 | "autoprefixer": "9.8.8", 30 | "babel-preset-env": "1.7.0", 31 | "babel-preset-preact": "2.0.0", 32 | "eslint": "9.27.0", 33 | "eslint-config-airbnb-base": "15.0.0", 34 | "eslint-config-prettier": "10.1.5", 35 | "eslint-plugin-import": "2.31.0", 36 | "eslint-plugin-json": "4.0.1", 37 | "eslint-plugin-prettier": "5.4.0", 38 | "eslint-plugin-react": "7.37.5", 39 | "jest-cli": "29.7.0", 40 | "node-sass": "9.0.0", 41 | "npm-run-all": "4.1.5", 42 | "parcel-bundler": "1.12.5", 43 | "postcss-modules": "3.2.2", 44 | "prettier": "3.5.3", 45 | "prettier-eslint": "16.4.2" 46 | }, 47 | "snyk": true 48 | } 49 | -------------------------------------------------------------------------------- /demos/moderator/postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoPrefixer = require("autoprefixer"); 2 | 3 | module.exports = { 4 | plugins: [autoPrefixer] 5 | }; 6 | -------------------------------------------------------------------------------- /demos/moderator/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | # show 10 slowest invocations: 4 | --durations=10 5 | 6 | # a bit of verbosity doesn't hurt: 7 | -v 8 | 9 | # report all the things == -rxXs: 10 | -ra 11 | 12 | # show values of the local vars in errors: 13 | --showlocals 14 | asyncio_mode = auto 15 | filterwarnings = 16 | error 17 | # All these would probably be fixed by https://github.com/aio-libs/aiohttp-demos/issues/188 18 | ignore:Please use `csr_matrix`:DeprecationWarning 19 | ignore:distutils Version classes are deprecated:DeprecationWarning 20 | ignore:`np.int` is a deprecated alias:DeprecationWarning 21 | ignore:Trying to unpickle:UserWarning 22 | testpaths = tests/ 23 | xfail_strict = true 24 | -------------------------------------------------------------------------------- /demos/moderator/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.11.18 2 | trafaret==2.1.1 3 | pyyaml==6.0.2 4 | numpy==1.26.4 5 | pandas==2.2.3 6 | scikit-learn==1.6.1 7 | scipy==1.15.3 8 | 9 | 10 | flake8==7.2.0 11 | flake8-bugbear==24.12.12 12 | flake8-quotes==3.4.0 13 | ipdb==0.13.13 14 | mypy==1.15.0 15 | pytest==8.3.5 16 | pytest-aiohttp==1.1.0 17 | pytest-asyncio==0.26.0 18 | pytest-cov==6.1.1 19 | pytest-sugar==1.0.0 20 | -------------------------------------------------------------------------------- /demos/moderator/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | def read_version(): 8 | regexp = re.compile(r"^__version__\W*=\W*'([\d.abrc]+)'") 9 | init_py = os.path.join(os.path.dirname(__file__), 10 | 'moderator', '__init__.py') 11 | with open(init_py) as f: 12 | for line in f: 13 | match = regexp.match(line) 14 | if match is not None: 15 | return match.group(1) 16 | msg = 'Cannot find version in moderator/__init__.py' 17 | raise RuntimeError(msg) 18 | 19 | 20 | install_requires = [ 21 | 'aiohttp', 22 | 'trafaret', 23 | 'pyyaml', 24 | 'numpy', 25 | 'pandas', 26 | 'scikit-learn', 27 | 'scipy', 28 | ] 29 | 30 | 31 | classifiers = [ 32 | 'License :: OSI Approved :: MIT License', 33 | 'Intended Audience :: Developers', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.5', 36 | 'Programming Language :: Python :: 3.6', 37 | 'Operating System :: POSIX', 38 | 'Development Status :: 3 - Alpha', 39 | 'Framework :: AsyncIO', 40 | ] 41 | 42 | 43 | setup(name='moderator', 44 | classifiers=classifiers, 45 | version=read_version(), 46 | description='Moderator AI app: UI and API', 47 | platforms=['POSIX'], 48 | author='Nikolay Novik', 49 | author_email='nickolainovik@gmail.com', 50 | url='https://github.com/jettify/moderator.ai', 51 | packages=find_packages(), 52 | include_package_data=True, 53 | install_requires=install_requires, 54 | license='Apache 2', 55 | zip_safe=False) 56 | -------------------------------------------------------------------------------- /demos/moderator/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/moderator/static/.gitkeep -------------------------------------------------------------------------------- /demos/moderator/static/App.d8c8d9ba.css: -------------------------------------------------------------------------------- 1 | body,html{margin:0;padding:0;font-family:Verdana,Geneva,Tahoma,sans-serif;font-size:14px}*{box-sizing:border-box}body{display:flex;flex-direction:column;align-items:center}._main_hhzf8_15{width:100%;max-width:600px;text-align:center}._textarea_hhzf8_20{padding:10px;width:100%;min-height:100px;resize:vertical}._submit_hhzf8_28,._textarea_hhzf8_20{border:1px solid #000;border-radius:2px}._submit_hhzf8_28{margin:20px 0;padding:10px 20px;color:#000;text-transform:uppercase;background-color:#fff}._results_1ah4m_1{display:flex;justify-content:space-between;margin:50px 0}._results_1ah4m_1 dd,._results_1ah4m_1 dt{margin:0}._result_1ah4m_1{display:flex;flex:1;flex-direction:column-reverse;align-items:center}._result_1ah4m_1 dd{font-size:3rem} -------------------------------------------------------------------------------- /demos/moderator/static/index.html: -------------------------------------------------------------------------------- 1 | Comments Moderator AI -------------------------------------------------------------------------------- /demos/moderator/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/moderator/tests/__init__.py -------------------------------------------------------------------------------- /demos/moderator/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from moderator.utils import load_config 3 | from moderator.consts import PROJ_ROOT 4 | from moderator.main import init 5 | 6 | 7 | @pytest.fixture 8 | def loop(event_loop): 9 | return event_loop 10 | 11 | 12 | @pytest.fixture 13 | def conf(): 14 | return load_config(PROJ_ROOT / 'config' / 'config.yml') 15 | 16 | 17 | @pytest.fixture 18 | def api(loop, aiohttp_client, conf): 19 | app = loop.run_until_complete(init(conf)) 20 | yield loop.run_until_complete(aiohttp_client(app)) 21 | loop.run_until_complete(app.shutdown()) 22 | -------------------------------------------------------------------------------- /demos/moderator/tests/test_basic.py: -------------------------------------------------------------------------------- 1 | 2 | async def test_index_page(api): 3 | resp = await api.get('/') 4 | assert resp.status == 200 5 | body = await resp.text() 6 | assert len(body) > 0 7 | 8 | 9 | async def test_moderate(api): 10 | payload = [{"comment": "xxx"}] 11 | resp = await api.post('/moderate', json=payload) 12 | assert resp.status == 200 13 | data = await resp.json() 14 | expected = [{ 15 | "identity_hate": "0.01", 16 | "insult": "0.04", 17 | "obscene": "0.04", 18 | "severe_toxic": "0.01", 19 | "toxic": "0.10" 20 | }] 21 | assert data == expected 22 | 23 | 24 | async def test_validation_error(api): 25 | payload = [{"body": "xxx"}] 26 | resp = await api.post('/moderate', json=payload) 27 | assert resp.status == 400 28 | data = await resp.json() 29 | e = {'error': {'0': {'body': 'body is not allowed key', 30 | 'comment': 'is required'}}} 31 | assert data == e 32 | -------------------------------------------------------------------------------- /demos/moderator/tests/test_pipeline.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from moderator.model.pipeline import build_pipeline, read_data 4 | 5 | 6 | def test_build_pipeline(): 7 | pipeline = build_pipeline() 8 | steps = pipeline.steps 9 | assert [s[0] for s in steps] == ['word_tfidf', 'logistic'] 10 | 11 | 12 | def test_train_model(): 13 | dataset_path = Path(__file__).parent / "toxic.csv" 14 | train, targets = read_data(dataset_path) 15 | 16 | pipeline = build_pipeline() 17 | pipeline.fit(train, targets) 18 | scores = pipeline.score(train, targets) 19 | assert scores 20 | -------------------------------------------------------------------------------- /demos/moderator/ui/App/App.scss: -------------------------------------------------------------------------------- 1 | $base-font-size: 14px; 2 | 3 | html, body { 4 | margin: 0; 5 | padding: 0; 6 | font-family: Verdana, Geneva, Tahoma, sans-serif; 7 | font-size: $base-font-size; 8 | } 9 | 10 | * { 11 | box-sizing: border-box; 12 | } 13 | 14 | body { 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | } 19 | 20 | .main { 21 | width: 100%; 22 | max-width: 600px; 23 | text-align: center; 24 | } 25 | 26 | .textarea { 27 | padding: 10px; 28 | width: 100%; 29 | min-height: 100px; 30 | resize: vertical; 31 | border: 1px solid black; 32 | border-radius: 2px; 33 | } 34 | 35 | .submit { 36 | margin: 20px 0; 37 | padding: 10px 20px; 38 | border: 1px solid black; 39 | color: black; 40 | text-transform: uppercase; 41 | background-color: white; 42 | border-radius: 2px; 43 | } 44 | -------------------------------------------------------------------------------- /demos/moderator/ui/App/Results/Results.scss: -------------------------------------------------------------------------------- 1 | .results { 2 | display: flex; 3 | justify-content: space-between; 4 | margin: 50px 0; 5 | 6 | dt, dd { 7 | margin: 0; 8 | } 9 | } 10 | 11 | .result { 12 | display: flex; 13 | flex: 1; 14 | flex-direction: column-reverse; 15 | align-items: center; 16 | 17 | dd { 18 | font-size: 3rem; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demos/moderator/ui/App/Results/index.js: -------------------------------------------------------------------------------- 1 | import "whatwg-fetch"; 2 | import { render, Component } from "preact"; 3 | import classNames from "./Results.scss"; 4 | 5 | export default class Results extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 |
toxic
11 |
{this.props.toxic}
12 |
13 |
14 |
severe toxic
15 |
{this.props.severe_toxic}
16 |
17 |
18 |
obscene
19 |
{this.props.obscene}
20 |
21 |
22 |
insult
23 |
{this.props.insult}
24 |
25 |
26 |
identity_hate
27 |
{this.props.identity_hate}
28 |
29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demos/moderator/ui/App/index.js: -------------------------------------------------------------------------------- 1 | import "whatwg-fetch"; 2 | import { render, Component } from "preact"; 3 | import classNames from "./App.scss"; 4 | import Results from "./Results"; 5 | 6 | class App extends Component { 7 | constructor() { 8 | super(); 9 | 10 | this.state = { 11 | comment: "", 12 | toxic: 0, 13 | severe_toxic: 0, 14 | obscene: 0, 15 | threat: 0, 16 | insult: 0, 17 | identity_hate: 0 18 | }; 19 | 20 | this.handleInput = this.handleInput.bind(this); 21 | this.handleSubmit = this.handleSubmit.bind(this); 22 | } 23 | 24 | handleInput(event) { 25 | this.setState({ 26 | comment: event.target.value 27 | }); 28 | } 29 | 30 | handleSubmit(event) { 31 | event.preventDefault(); 32 | fetch("/moderate", { 33 | method: "POST", 34 | body: JSON.stringify([{ comment: this.state.comment }]) 35 | }) 36 | .then(response => response.text()) 37 | .then(jsonData => JSON.parse(jsonData)) 38 | .then(data => { 39 | this.setState({ 40 | toxic: data[0].toxic, 41 | severe_toxic: data[0].severe_toxic, 42 | obscene: data[0].obscene, 43 | threat: data[0].threat, 44 | insult: data[0].insult, 45 | identity_hate: data[0].identity_hate 46 | }); 47 | }); 48 | } 49 | 50 | render() { 51 | return ( 52 |
53 |

Moderator AI

54 | 55 | 63 | 64 |
69 |