├── .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 |
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 |
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 | 
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 |
10 | ▶
11 |
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 |
81 |
82 | );
83 | }
84 | }
85 |
86 | render( , document.body);
87 |
--------------------------------------------------------------------------------
/demos/moderator/ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Comments Moderator AI
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demos/moderator_bot/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.py]
12 | indent_size = 4
13 |
14 | [Makefile]
15 | indent_style = tab
16 |
17 | [*.md]
18 | trim_trailing_whitespace = false
19 |
--------------------------------------------------------------------------------
/demos/moderator_bot/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/demos/moderator_bot/.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 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
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 | .static_storage/
56 | .media/
57 | local_settings.py
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | sample/
107 | .vscode/
108 | .pytest_cache/
109 |
--------------------------------------------------------------------------------
/demos/moderator_bot/README.md:
--------------------------------------------------------------------------------
1 | ## moderator_bot
2 |
3 | aiohttp demo for simple Slack bot that moderate toxic messages.
4 | It uses Events API.
5 |
6 | Model reused from [Moderator AI](https://github.com/aio-libs/aiohttp-demos/tree/master/demos/moderator) project.
7 |
8 | 
9 |
10 |
11 | ## Requirements
12 | - [aiohttp](https://github.com/aio-libs/aiohttp)
13 | - [slack_sdk](https://github.com/slackapi/python-slack-sdk)
14 |
15 |
16 | ## Prerequisites
17 | Before running bot, you need to setup slack app for it and obtain `SLACK_BOT_TOKEN`. You can read detailed guide on how to do it here https://api.slack.com/bot-users.
18 |
19 | Also you need `GIPHY_API_KEY` that you can get here https://developers.giphy.com/.
20 |
21 | ## Starting bot
22 | Clone the repo and after do
23 |
24 | ```shell
25 | $ cd moderator_bot
26 | $ pip install -r requirements-dev.txt
27 | ```
28 | With credentials from previous section provide proper environment variables
29 |
30 | ```shell
31 | $ set -x SLACK_BOT_TOKEN xxx-xxx
32 | $ set -x GIPHY_API_KEY xxx-xxx
33 | ```
34 |
35 | Start app
36 | ```shell
37 | $ python -m moderator_bot
38 | ```
39 |
40 | ## Testing
41 | ```shell
42 | $ pytest -s tests/
43 | ```
44 |
--------------------------------------------------------------------------------
/demos/moderator_bot/configs/base.yml:
--------------------------------------------------------------------------------
1 | host: 127.0.0.1
2 | port: 8080
3 |
4 | debug: true
5 |
6 | request_timeout: 10
7 |
8 | model_path: "demos/moderator_bot/model/pipeline.dat"
9 |
--------------------------------------------------------------------------------
/demos/moderator_bot/model/pipeline.dat:
--------------------------------------------------------------------------------
1 | ../../moderator/model/pipeline.dat
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/moderator_bot/moderator_bot/__init__.py
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/__main__.py:
--------------------------------------------------------------------------------
1 | from moderator_bot.server import main
2 |
3 | if __name__ == "__main__":
4 | main()
5 |
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/app.py:
--------------------------------------------------------------------------------
1 | from .server import init_application
2 | from .settings import PROJECT_ROOT
3 | from .utils import load_config
4 |
5 |
6 | async def get_app():
7 | """Used by aiohttp-devtools for local development."""
8 | import aiohttp_debugtoolbar
9 | config = load_config(PROJECT_ROOT / "configs" / "base.yml")
10 | app = await init_application(config)
11 | aiohttp_debugtoolbar.setup(app)
12 | return app
13 |
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/giphy.py:
--------------------------------------------------------------------------------
1 | from functools import partialmethod
2 |
3 | import aiohttp
4 | import async_timeout
5 | from yarl import URL
6 |
7 |
8 | class GiphyClient:
9 | def __init__(self, api_key, timeout, session=None):
10 | self.api_key = api_key
11 | self.timeout = timeout
12 | self.base_url = URL("https://api.giphy.com/v1/gifs")
13 |
14 | if session is None:
15 | session = aiohttp.ClientSession()
16 |
17 | self.session = session
18 |
19 | async def _request(self, method, part, *args, **kwargs):
20 | url = self.base_url / part
21 |
22 | kwargs.setdefault("params", {})
23 | kwargs["params"] = {
24 | "api_key": self.api_key,
25 | **kwargs["params"]
26 | }
27 |
28 | async with async_timeout.timeout(self.timeout):
29 | async with self.session.request(
30 | method,
31 | url,
32 | *args,
33 | **kwargs
34 | ) as resp:
35 | if "application/json" in resp.content_type:
36 | return await resp.json()
37 |
38 | get = partialmethod(_request, "GET")
39 |
40 | async def close(self):
41 | await self.session.close()
42 |
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/handlers.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import numpy as np
4 | from aiohttp import web
5 |
6 | from .worker import predict
7 |
8 |
9 | class MainHandler:
10 | def __init__(self, executor, slack_client, giphy_client):
11 | self.executor = executor
12 | self.slack_client = slack_client
13 | self.giphy_client = giphy_client
14 | self._toxicity_index = 0.4
15 |
16 | async def listen_message(self, request):
17 | post = await request.json()
18 |
19 | if "challenge" in post:
20 | return web.Response(body=post["challenge"])
21 |
22 | if post["event"]["type"] == "message":
23 | await self._message_handler(post["event"])
24 |
25 | return web.Response()
26 |
27 | async def _respond(self, event):
28 | result = await self.giphy_client.get(
29 | "random",
30 | params={
31 | "tag": "funny cat"
32 | },
33 | )
34 | image_url = result["data"]["image_url"]
35 |
36 | text = (f"Hey <@{event['user']}>, please be polite! "
37 | f"Here is a funny cat GIF for you {image_url}")
38 |
39 | await self.slack_client.chat_postMessage(
40 | channel=event["channel"],
41 | text=text,
42 | )
43 |
44 | async def _message_handler(self, event):
45 | loop = asyncio.get_running_loop()
46 | scores = await loop.run_in_executor(self.executor, predict, event["text"])
47 | if np.average([scores.toxic, scores.insult]) >= self._toxicity_index:
48 | await self._respond(event)
49 |
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/router.py:
--------------------------------------------------------------------------------
1 | def setup_main_handler(app, handler):
2 | app.router.add_post(
3 | "/listen",
4 | handler.listen_message,
5 | name="listen",
6 | )
7 |
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/server.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from concurrent.futures import ProcessPoolExecutor
3 | from functools import partial
4 | from pathlib import Path
5 |
6 | from aiohttp import web
7 | from slack_sdk.web.async_client import AsyncWebClient
8 |
9 | from .giphy import GiphyClient
10 | from .handlers import MainHandler
11 | from .router import setup_main_handler
12 | from .settings import GIPHY_API_KEY, MAX_WORKERS, PROJECT_ROOT, SLACK_BOT_TOKEN
13 | from .utils import load_config
14 | from .worker import warm
15 |
16 |
17 | def setup_cleanup_hooks(tasks):
18 | async def cleanup(app):
19 | for func in tasks:
20 | result = func()
21 | if asyncio.iscoroutine(result):
22 | await result
23 |
24 | return cleanup
25 |
26 |
27 | def setup_startup_hooks(executor, model_path, workers_count):
28 | async def startup(app):
29 | run = partial(asyncio.get_running_loop().run_in_executor, executor, warm, model_path)
30 | coros = [run() for _ in range(0, workers_count)]
31 | await asyncio.gather(*coros)
32 |
33 | return startup
34 |
35 |
36 | async def init_application(config):
37 | app = web.Application()
38 |
39 | executor = ProcessPoolExecutor(MAX_WORKERS)
40 |
41 | slack_client = AsyncWebClient(SLACK_BOT_TOKEN)
42 | giphy_client = GiphyClient(GIPHY_API_KEY, config["request_timeout"])
43 |
44 | handler = MainHandler(executor, slack_client, giphy_client)
45 |
46 | model_path = Path(config["model_path"])
47 |
48 | setup_main_handler(app, handler)
49 |
50 | app.on_startup.append(setup_startup_hooks(
51 | executor,
52 | model_path,
53 | MAX_WORKERS,
54 | ))
55 |
56 | app.on_cleanup.append(setup_cleanup_hooks([
57 | partial(executor.shutdown, wait=True),
58 | giphy_client.close,
59 | ]))
60 |
61 | return app
62 |
63 |
64 | def main():
65 | config = load_config(PROJECT_ROOT / "configs" / "base.yml")
66 | app = asyncio.run(init_application(config))
67 | web.run_app(app, host=config["host"], port=config["port"])
68 |
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 |
4 | from .utils import required_env
5 |
6 | PROJECT_ROOT = Path(__file__).parent.parent
7 |
8 | MAX_WORKERS = os.cpu_count()
9 |
10 | SLACK_BOT_TOKEN = required_env("SLACK_BOT_TOKEN")
11 |
12 | GIPHY_API_KEY = required_env("GIPHY_API_KEY")
13 |
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 |
4 | import yaml
5 |
6 |
7 | def load_config(path):
8 | with Path(path).open() as fp:
9 | return yaml.safe_load(fp.read())
10 |
11 |
12 | def required_env(variable):
13 | value = os.environ.get(variable)
14 | if value is None:
15 | raise RuntimeError(f"{variable} is required to start the service.")
16 | return value
17 |
--------------------------------------------------------------------------------
/demos/moderator_bot/moderator_bot/worker.py:
--------------------------------------------------------------------------------
1 | import pickle
2 | from collections import namedtuple
3 |
4 | import numpy as np
5 |
6 | _model = None
7 |
8 | Scores = namedtuple("Scores", ["toxic", "severe_toxic",
9 | "obscence", "insult", "identity_hate"])
10 |
11 |
12 | def warm(model_path):
13 | global _model
14 | if _model is None:
15 | with model_path.open('rb') as fp:
16 | pipeline = pickle.load(fp)
17 | _model = pipeline
18 | return True
19 |
20 |
21 | def predict(message):
22 | results = _model.predict_proba([message])
23 | results = np.array(results).T[1].tolist()[0]
24 | return Scores(*results)
25 |
--------------------------------------------------------------------------------
/demos/moderator_bot/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 | # aioslacker needs updating.
18 | ignore:"@coroutine" decorator is deprecated since Python 3.8:DeprecationWarning
19 | ignore:The loop argument is deprecated since Python 3.8:DeprecationWarning
20 | # All these would probably be fixed by https://github.com/aio-libs/aiohttp-demos/issues/188
21 | ignore:Please use `csr_matrix`:DeprecationWarning
22 | ignore:distutils Version classes are deprecated:DeprecationWarning
23 | ignore:`np.int` is a deprecated alias:DeprecationWarning
24 | ignore:Trying to unpickle:UserWarning
25 | testpaths = tests/
26 | xfail_strict = true
27 |
--------------------------------------------------------------------------------
/demos/moderator_bot/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | -i https://pypi.org/simple/
2 | aiohttp==3.11.18
3 | async-timeout==5.0.1
4 | atomicwrites==1.4.1
5 | attrs==25.3.0
6 | certifi==2025.4.26
7 | chardet==5.2.0
8 | idna==3.10
9 | importlib-metadata==0.23 ; python_version < '3.8'
10 | joblib==1.5.1
11 | more-itertools==10.7.0
12 | multidict==6.4.4
13 | numpy==1.26.4
14 | pluggy==1.6.0
15 | pytest==8.3.5
16 | pytest-aiohttp==1.1.0
17 | pyyaml==6.0.2
18 | scikit-learn==1.6.1
19 | scipy==1.15.3
20 | six==1.17.0
21 | slack_sdk==3.35.0
22 | urllib3==2.4.0
23 | yarl==1.20.0
24 | zipp==3.22.0
25 |
--------------------------------------------------------------------------------
/demos/moderator_bot/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/moderator_bot/tests/__init__.py
--------------------------------------------------------------------------------
/demos/moderator_bot/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from moderator_bot.server import init_application
3 | from moderator_bot.settings import PROJECT_ROOT
4 | from moderator_bot.utils import load_config
5 |
6 |
7 | @pytest.fixture
8 | def config():
9 | return load_config(PROJECT_ROOT / "configs" / "base.yml")
10 |
11 |
12 | @pytest.fixture
13 | async def client(aiohttp_client, config):
14 | app = await init_application(config)
15 | return await aiohttp_client(app)
16 |
--------------------------------------------------------------------------------
/demos/moderator_bot/tests/test_handlers.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 |
3 |
4 | async def test_listen_challenge(client):
5 | resp = await client.post("/listen", json={"challenge": "challenge"})
6 | result = await resp.text()
7 | assert "challenge" in result
8 |
9 |
10 | async def test_list_message(client):
11 | with mock.patch('moderator_bot.handlers.MainHandler._respond') as m:
12 | await client.post(
13 | "/listen",
14 | json={
15 | "event": {
16 | "type": "message",
17 | "text": "You are stupid and useless!"
18 | },
19 | },
20 | )
21 | assert m.called
22 |
--------------------------------------------------------------------------------
/demos/motortwit/Makefile:
--------------------------------------------------------------------------------
1 | # Some simple testing tasks (sorry, UNIX only).
2 |
3 | FLAGS=
4 |
5 |
6 | flake:
7 | flake8 motortwit setup.py
8 |
9 | clean:
10 | rm -rf `find . -name __pycache__`
11 | rm -f `find . -type f -name '*.py[co]' `
12 | rm -f `find . -type f -name '*~' `
13 | rm -f `find . -type f -name '.*~' `
14 | rm -f `find . -type f -name '@*' `
15 | rm -f `find . -type f -name '#*#' `
16 | rm -f `find . -type f -name '*.orig' `
17 | rm -f `find . -type f -name '*.rej' `
18 | rm -f .coverage
19 | rm -rf coverage
20 | rm -rf build
21 | rm -rf htmlcov
22 | rm -rf dist
23 |
24 | run:
25 | python -m motortwit
26 |
27 | fake_data:
28 | python ./motortwit/generate_data.py
29 |
30 | docker_start_mongo:
31 | docker-compose -f docker-compose.yml up -d mongo
32 |
33 | docker_stop_mongo:
34 | docker-compose -f docker-compose.yml stop mongo
35 |
36 | .PHONY: flake clean
37 |
--------------------------------------------------------------------------------
/demos/motortwit/README.rst:
--------------------------------------------------------------------------------
1 | Motortwit Demo
2 | ==============
3 |
4 | Example of mongo project using aiohttp_, motor_ and aiohttp_jinja2_,
5 | similar to flask one. Here I assume you have *python3.5* and *docker* installed.
6 |
7 | Installation
8 | ============
9 |
10 | Clone repo and install library::
11 |
12 | $ git clone git@github.com:aio-libs/aiohttp-demos.git
13 | $ cd aiohttp-demos
14 |
15 | Install the app::
16 |
17 | $ cd demos/motortwit
18 | $ pip install -e .
19 |
20 | Create database for your project::
21 |
22 | make docker_start_mongo
23 | make fake_data
24 |
25 |
26 | Run application::
27 |
28 | $ make run
29 |
30 | Open browser::
31 |
32 | http://127.0.0.1:9001
33 |
34 |
35 | Requirements
36 | ============
37 | * aiohttp_
38 | * motor_
39 | * aiohttp_jinja2_
40 |
41 |
42 | .. _Python: https://www.python.org
43 | .. _aiohttp: https://github.com/KeepSafe/aiohttp
44 | .. _motor: https://github.com/mongodb/motor
45 | .. _aiohttp_jinja2: https://github.com/aio-libs/aiohttp_jinja2
46 |
--------------------------------------------------------------------------------
/demos/motortwit/config/config.yml:
--------------------------------------------------------------------------------
1 | mongo:
2 | host: 127.0.0.1
3 | port: 27017
4 | database: "motortwit"
5 | max_pool_size: 5
6 |
7 | host: 127.0.0.1
8 | port: 9001
9 |
--------------------------------------------------------------------------------
/demos/motortwit/docker-compose.yml:
--------------------------------------------------------------------------------
1 | mongo:
2 | image: mongo:3.4
3 | ports:
4 | - 27017:27017
5 | command: [--smallfiles]
6 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.0.1'
2 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/__main__.py:
--------------------------------------------------------------------------------
1 | from motortwit.main import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/db.py:
--------------------------------------------------------------------------------
1 | import trafaret as t
2 | from trafaret.contrib.object_id import MongoId
3 | from trafaret.contrib.rfc_3339 import DateTime
4 |
5 |
6 | user = t.Dict({
7 | t.Key('_id'): MongoId(),
8 | t.Key('username'): t.String(max_length=50),
9 | t.Key('email'): t.Email,
10 | t.Key('pw_hash'): t.String(),
11 | })
12 |
13 |
14 | message = t.Dict({
15 | t.Key('_id'): MongoId(),
16 | t.Key('author_id'): MongoId(),
17 | t.Key('username'): t.String(max_length=50),
18 | t.Key('text'): t.String(),
19 | t.Key('pub_date'): DateTime(),
20 | })
21 |
22 | follower = t.Dict({
23 | t.Key('_id'): MongoId(),
24 | t.Key('who_id'): MongoId(),
25 | t.Key('whom_id'): t.List(MongoId()),
26 | })
27 |
28 |
29 | async def get_user_id(user_collection, username):
30 | rv = await (user_collection.find_one(
31 | {'username': username},
32 | {'_id': 1}))
33 | return rv['_id'] if rv else None
34 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/main.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | import pathlib
4 |
5 | import aiohttp_jinja2
6 | import jinja2
7 | from aiohttp import web
8 | from aiohttp_security import setup as setup_security
9 | from aiohttp_security import CookiesIdentityPolicy
10 |
11 | from motortwit.routes import setup_routes
12 | from motortwit.security import AuthorizationPolicy
13 | from motortwit.utils import (format_datetime, init_mongo, load_config,
14 | robo_avatar_url)
15 | from motortwit.views import SiteHandler
16 |
17 |
18 | PROJ_ROOT = pathlib.Path(__file__).parent.parent
19 | TEMPLATES_ROOT = pathlib.Path(__file__).parent / 'templates'
20 |
21 |
22 | async def setup_mongo(app, conf, loop):
23 | mongo = await init_mongo(conf['mongo'], loop)
24 |
25 | async def close_mongo(app):
26 | mongo.client.close()
27 |
28 | app.on_cleanup.append(close_mongo)
29 | return mongo
30 |
31 |
32 | def setup_jinja(app):
33 | jinja_env = aiohttp_jinja2.setup(
34 | app, loader=jinja2.FileSystemLoader(str(TEMPLATES_ROOT)))
35 |
36 | jinja_env.filters['datetimeformat'] = format_datetime
37 | jinja_env.filters['robo_avatar_url'] = robo_avatar_url
38 |
39 |
40 | async def init(loop):
41 | conf = load_config(PROJ_ROOT / 'config' / 'config.yml')
42 |
43 | app = web.Application(loop=loop)
44 | mongo = await setup_mongo(app, conf, loop)
45 |
46 | setup_jinja(app)
47 | setup_security(app, CookiesIdentityPolicy(), AuthorizationPolicy(mongo))
48 |
49 | # setup views and routes
50 | handler = SiteHandler(mongo)
51 | setup_routes(app, handler, PROJ_ROOT)
52 | host, port = conf['host'], conf['port']
53 | return app, host, port
54 |
55 |
56 | async def get_app():
57 | """Used by aiohttp-devtools for local development."""
58 | import aiohttp_debugtoolbar
59 | app, _, _ = await init(asyncio.get_event_loop())
60 | aiohttp_debugtoolbar.setup(app)
61 | return app
62 |
63 |
64 | def main():
65 | logging.basicConfig(level=logging.DEBUG)
66 |
67 | loop = asyncio.get_event_loop()
68 | app, host, port = loop.run_until_complete(init(loop))
69 | web.run_app(app, host=host, port=port)
70 |
71 |
72 | if __name__ == '__main__':
73 | main()
74 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/routes.py:
--------------------------------------------------------------------------------
1 | def setup_routes(app, handler, project_root):
2 | router = app.router
3 | h = handler
4 | router.add_get('/', h.timeline, name='timeline')
5 | router.add_get('/login', h.login_page, name='login')
6 | router.add_get('/logout', h.logout, name='logout')
7 | router.add_get('/public', h.public_timeline, name='public_timeline')
8 | router.add_get('/register', h.register_page, name='register')
9 | router.add_get('/{username}', h.user_timeline, name='user_timeline')
10 | router.add_get('/{username}/follow', h.follow_user, name='follow_user')
11 | router.add_get('/{username}/unfollow', h.unfollow_user,
12 | name='unfollow_user')
13 | router.add_post('/add_message', h.add_message, name='add_message')
14 | router.add_post('/login', h.login)
15 | router.add_post('/register', h.register)
16 | router.add_static('/static/', path=str(project_root / 'static'),
17 | name='static')
18 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/security.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import functools
3 |
4 | import bcrypt
5 | from aiohttp import web
6 | from aiohttp_security import authorized_userid
7 | from aiohttp_security.abc import AbstractAuthorizationPolicy
8 | from bson import ObjectId
9 |
10 |
11 | def generate_password_hash(password, salt_rounds=12):
12 | password_bin = password.encode('utf-8')
13 | hashed = bcrypt.hashpw(password_bin, bcrypt.gensalt(salt_rounds))
14 | encoded = base64.b64encode(hashed)
15 | return encoded.decode('utf-8')
16 |
17 |
18 | def check_password_hash(encoded, password):
19 | password = password.encode('utf-8')
20 | encoded = encoded.encode('utf-8')
21 |
22 | hashed = base64.b64decode(encoded)
23 | is_correct = bcrypt.hashpw(password, hashed) == hashed
24 | return is_correct
25 |
26 |
27 | class AuthorizationPolicy(AbstractAuthorizationPolicy):
28 | def __init__(self, mongo):
29 | self.mongo = mongo
30 |
31 | async def authorized_userid(self, identity):
32 | user = await self.mongo.user.find_one({'_id': ObjectId(identity)})
33 | if user:
34 | return identity
35 | return None
36 |
37 | async def permits(self, identity, permission, context=None):
38 | if identity is None:
39 | return False
40 | return True
41 |
42 |
43 | def auth_required(f):
44 | @functools.wraps(f)
45 | async def wrapped(self, request):
46 | user_id = await authorized_userid(request)
47 | if not user_id:
48 | raise web.HTTPUnauthorized()
49 | return (await f(self, request))
50 | return wrapped
51 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 | {% block title %}Welcome{% endblock %} | MiniTwit
3 |
4 |
5 |
MotorTwit
6 |
17 |
18 | {% block body %}{% endblock %}
19 |
20 |
23 |
24 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Sign In{% endblock %}
3 | {% block body %}
4 | Sign In
5 | {% if error %}Error: {{ error }}
{% endif %}
6 |
7 |
8 | Username:
9 |
10 | Password:
11 |
12 |
13 |
14 |
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/templates/register.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Sign Up{% endblock %}
3 | {% block body %}
4 | Sign Up
5 | {% if error %}Error: {{ error }}
{% endif %}
6 |
7 |
8 | Username:
9 |
10 | E-Mail:
11 |
12 | Password:
13 |
14 | Password (repeat) :
15 |
16 |
17 |
18 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/templates/timeline.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}
3 | {% if endpoint == 'public_timeline' %}
4 | Public Timeline
5 | {% elif endpoint == 'user_timeline' %}
6 | {{ profile_user.username }}'s Timeline
7 | {% else %}
8 | My Timeline
9 | {% endif %}
10 | {% endblock %}
11 | {% block body %}
12 | {{ self.title() }}
13 | {% if user %}
14 | {% if endpoint == 'user_timeline' %}
15 |
16 | {% if user._id == profile_user._id %}
17 | This is you!
18 | {% elif followed %}
19 | You are currently following this user.
20 |
Unfollow user .
22 | {% else %}
23 | You are not yet following this user.
24 |
Follow user .
26 | {% endif %}
27 |
28 | {% elif endpoint == 'timeline' %}
29 |
36 | {% endif %}
37 | {% endif %}
38 |
49 | {% endblock %}
50 |
--------------------------------------------------------------------------------
/demos/motortwit/motortwit/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | from hashlib import md5
3 |
4 | import motor.motor_asyncio as aiomotor
5 | import pytz
6 | import yaml
7 | from aiohttp import web
8 | from dateutil.parser import parse
9 |
10 | from . import db
11 |
12 |
13 | def load_config(fname):
14 | with open(fname, 'rt') as f:
15 | data = yaml.load(f)
16 | # TODO: add config validation
17 | return data
18 |
19 |
20 | async def init_mongo(conf, loop):
21 | host = os.environ.get('DOCKER_MACHINE_IP', '127.0.0.1')
22 | conf['host'] = host
23 | mongo_uri = "mongodb://{}:{}".format(conf['host'], conf['port'])
24 | conn = aiomotor.AsyncIOMotorClient(
25 | mongo_uri,
26 | maxPoolSize=conf['max_pool_size'],
27 | io_loop=loop)
28 | db_name = conf['database']
29 | return conn[db_name]
30 |
31 |
32 | def robo_avatar_url(user_data, size=80):
33 | """Return the gravatar image for the given email address."""
34 | hash = md5(str(user_data).strip().lower().encode('utf-8')).hexdigest()
35 | url = "https://robohash.org/{hash}.png?size={size}x{size}".format(
36 | hash=hash, size=size)
37 | return url
38 |
39 |
40 | def format_datetime(timestamp):
41 | if isinstance(timestamp, str):
42 | timestamp = parse(timestamp)
43 | return timestamp.replace(tzinfo=pytz.utc).strftime('%Y-%m-%d @ %H:%M')
44 |
45 |
46 | def redirect(request, name, **kw):
47 | router = request.app.router
48 | location = router[name].url_for(**kw)
49 | return web.HTTPFound(location=location)
50 |
51 |
52 | async def validate_register_form(mongo, form):
53 | error = None
54 | user_id = await db.get_user_id(mongo.user, form['username'])
55 |
56 | if not form['username']:
57 | error = 'You have to enter a username'
58 | elif not form['email'] or '@' not in form['email']:
59 | error = 'You have to enter a valid email address'
60 | elif not form['password']:
61 | error = 'You have to enter a password'
62 | elif form['password'] != form['password2']:
63 | error = 'The two passwords do not match'
64 | elif user_id is not None:
65 | error = 'The username is already taken'
66 | return error
67 |
--------------------------------------------------------------------------------
/demos/motortwit/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/motortwit/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.11.18
2 | aiohttp-jinja2==1.6
3 | aiohttp-security==0.5.0
4 | bcrypt==4.3.0
5 | faker==37.3.0
6 | motor==3.7.1
7 | pytz==2025.2
8 | pyyaml==6.0.2
9 | trafaret==2.1.1
10 |
--------------------------------------------------------------------------------
/demos/motortwit/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 | 'motortwit', '__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 motortwit/__init__.py'
17 | raise RuntimeError(msg)
18 |
19 |
20 | install_requires = ['aiohttp',
21 | 'pytz',
22 | 'bcrypt',
23 | 'aiohttp_security',
24 | 'trafaret',
25 | 'aiohttp_jinja2',
26 | 'pyyaml',
27 | 'motor',
28 | 'faker']
29 |
30 |
31 | setup(name='motortwit',
32 | version=read_version(),
33 | description='Blog project example from aiohttp_admin',
34 | platforms=['POSIX'],
35 | packages=find_packages(),
36 | include_package_data=True,
37 | install_requires=install_requires,
38 | zip_safe=False)
39 |
--------------------------------------------------------------------------------
/demos/motortwit/tests/test_motortwit.py:
--------------------------------------------------------------------------------
1 | async def test_something():
2 | # TODO: Create some actual tests.
3 | assert True
4 |
--------------------------------------------------------------------------------
/demos/polls/Makefile:
--------------------------------------------------------------------------------
1 | # Some simple testing tasks (sorry, UNIX only).
2 |
3 | FLAGS=
4 |
5 |
6 | flake:
7 | flake8 aiohttpdemo_polls
8 |
9 | test:
10 | pytest tests
11 |
12 | clean:
13 | rm -rf `find . -name __pycache__`
14 | rm -f `find . -type f -name '*.py[co]' `
15 | rm -f `find . -type f -name '*~' `
16 | rm -f `find . -type f -name '.*~' `
17 | rm -f `find . -type f -name '@*' `
18 | rm -f `find . -type f -name '#*#' `
19 | rm -f `find . -type f -name '*.orig' `
20 | rm -f `find . -type f -name '*.rej' `
21 | rm -f .coverage
22 | rm -rf coverage
23 | rm -rf build
24 | rm -rf htmlcov
25 | rm -rf dist
26 |
27 | .PHONY: flake clean test
28 |
--------------------------------------------------------------------------------
/demos/polls/README.rst:
--------------------------------------------------------------------------------
1 | Polls
2 | =====
3 |
4 | Example of polls project using aiohttp_, asyncpg_, SQLAlchemy_ and aiohttp_jinja2_,
5 | similar to Django one.
6 |
7 |
8 | Preparations
9 | ------------
10 |
11 | Details could be found in `Preparations `_.
12 |
13 | In short.
14 |
15 | Run Postgres DB server::
16 |
17 | $ docker run --rm -it -p 5432:5432 postgres:10
18 |
19 | Create db and populate it with sample data::
20 |
21 | $ python init_db.py
22 |
23 |
24 | Run
25 | ---
26 | Run application::
27 |
28 | $ python -m aiohttpdemo_polls
29 |
30 | Open browser::
31 |
32 | http://localhost:8080/
33 |
34 | .. image:: https://raw.githubusercontent.com/aio-libs/aiohttp-demos/master/docs/_static/polls.png
35 | :align: center
36 | :width: 460px
37 |
38 | Tests
39 | -----
40 |
41 | .. code-block:: shell
42 |
43 | $ pytest tests
44 |
45 | or:
46 |
47 | .. code-block:: shell
48 |
49 | $ pip install tox
50 | $ tox
51 |
52 |
53 | Development
54 | -----------
55 | Please review general contribution info at `README `_.
56 |
57 |
58 | Also for illustration purposes it is useful to show project structure when it changes,
59 | like `here `_.
60 | Here is how you can do that::
61 |
62 | $ tree -I "__pycache__|aiohttpdemo_polls.egg-info" --dirsfirst
63 |
64 |
65 | Requirements
66 | ============
67 | * aiohttp_
68 | * asyncpg_
69 | * aiohttp_jinja2_
70 | * SQLAlchemy_
71 |
72 |
73 | .. _Python: https://www.python.org
74 | .. _aiohttp: https://github.com/aio-libs/aiohttp
75 | .. _asyncpg: https://pypi.org/project/asyncpg
76 | .. _aiohttp_jinja2: https://github.com/aio-libs/aiohttp_jinja2
77 | .. _SQLAlchemy: https://www.sqlalchemy.org/
78 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.0.1'
2 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from aiohttpdemo_polls.main import main
4 |
5 |
6 | main(sys.argv[1:])
7 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/db.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 |
3 | from sqlalchemy import ForeignKey, String, select
4 | from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
5 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
6 |
7 | from aiohttpdemo_polls.typedefs import config_key, db_key
8 |
9 |
10 | class Base(DeclarativeBase):
11 | pass
12 |
13 |
14 | class Question(Base):
15 | __tablename__ = "question"
16 |
17 | id: Mapped[int] = mapped_column(primary_key=True)
18 | question_text: Mapped[str] = mapped_column(String(200), nullable=False)
19 | pub_date: Mapped[date]
20 |
21 |
22 | class Choice(Base):
23 | __tablename__ = "choice"
24 |
25 | id: Mapped[int] = mapped_column(primary_key=True)
26 | choice_text: Mapped[str] = mapped_column(String(200), nullable=False)
27 | votes: Mapped[int] = mapped_column(server_default="0", nullable=False)
28 |
29 | question_id: Mapped[int] = mapped_column(
30 | ForeignKey("question.id", ondelete="CASCADE")
31 | )
32 |
33 |
34 | class RecordNotFound(Exception):
35 | """Requested record in database was not found"""
36 |
37 |
38 | DSN = "postgresql+asyncpg://{user}:{password}@{host}:{port}/{database}"
39 |
40 |
41 | async def pg_context(app):
42 | engine = create_async_engine(DSN.format(**app[config_key]["postgres"]))
43 | app[db_key] = async_sessionmaker(engine)
44 |
45 | yield
46 |
47 | await engine.dispose()
48 |
49 |
50 | async def get_question(sess, question_id):
51 | result = await sess.scalars(select(Question).where(Question.id == question_id))
52 | question_record = result.first()
53 | if not question_record:
54 | msg = "Question with id: {} does not exists"
55 | raise RecordNotFound(msg.format(question_id))
56 | result = await sess.scalars(
57 | select(Choice).where(Choice.question_id == question_id).order_by(Choice.id)
58 | )
59 | choice_records = result.all()
60 | return question_record, choice_records
61 |
62 |
63 | async def vote(app, question_id, choice_id):
64 | async with app[db_key].begin() as sess:
65 | result = await sess.get(Choice, choice_id)
66 | result.votes += 1
67 | if not result:
68 | msg = "Question with id: {} or choice id: {} does not exists"
69 | raise RecordNotFound(msg.format(question_id, choice_id))
70 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/main.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 |
4 | import aiohttp_jinja2
5 | import jinja2
6 | from aiohttp import web
7 |
8 | from aiohttpdemo_polls.db import pg_context
9 | from aiohttpdemo_polls.middlewares import setup_middlewares
10 | from aiohttpdemo_polls.routes import setup_routes, setup_static_routes
11 | from aiohttpdemo_polls.settings import get_config
12 | from aiohttpdemo_polls.typedefs import config_key
13 |
14 |
15 | async def init_app(argv=None):
16 |
17 | app = web.Application()
18 |
19 | app[config_key] = get_config(argv)
20 |
21 | # setup Jinja2 template renderer
22 | aiohttp_jinja2.setup(
23 | app, loader=jinja2.PackageLoader('aiohttpdemo_polls', 'templates'))
24 |
25 | # create db connection on startup, shutdown on exit
26 | app.cleanup_ctx.append(pg_context)
27 |
28 | # setup views and routes
29 | setup_routes(app)
30 | setup_static_routes(app)
31 | setup_middlewares(app)
32 |
33 | return app
34 |
35 |
36 | async def get_app():
37 | """Used by aiohttp-devtools for local development."""
38 | import aiohttp_debugtoolbar
39 | app = await init_app(sys.argv[1:])
40 | aiohttp_debugtoolbar.setup(app)
41 | return app
42 |
43 |
44 | def main(argv):
45 | logging.basicConfig(level=logging.DEBUG)
46 |
47 | app = init_app(argv)
48 |
49 | config = get_config(argv)
50 | web.run_app(app,
51 | host=config['host'],
52 | port=config['port'])
53 |
54 |
55 | if __name__ == '__main__':
56 | main(sys.argv[1:])
57 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/middlewares.py:
--------------------------------------------------------------------------------
1 | # middlewares.py
2 | import aiohttp_jinja2
3 | from aiohttp import web
4 |
5 |
6 | async def handle_404(request):
7 | return aiohttp_jinja2.render_template('404.html', request, {}, status=404)
8 |
9 |
10 | async def handle_500(request):
11 | return aiohttp_jinja2.render_template('500.html', request, {}, status=500)
12 |
13 |
14 | def create_error_middleware(overrides):
15 |
16 | @web.middleware
17 | async def error_middleware(request, handler):
18 | try:
19 | return await handler(request)
20 | except web.HTTPException as ex:
21 | override = overrides.get(ex.status)
22 | if override:
23 | return await override(request)
24 |
25 | raise
26 | except Exception:
27 | request.protocol.logger.exception("Error handling request")
28 | return await overrides[500](request)
29 |
30 | return error_middleware
31 |
32 |
33 | def setup_middlewares(app):
34 | error_middleware = create_error_middleware({
35 | 404: handle_404,
36 | 500: handle_500
37 | })
38 | app.middlewares.append(error_middleware)
39 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/routes.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 |
3 | from aiohttpdemo_polls.views import index, poll, results, vote
4 |
5 | PROJECT_ROOT = pathlib.Path(__file__).parent
6 |
7 |
8 | def setup_routes(app):
9 | app.router.add_get("/", index)
10 | app.router.add_get("/poll/{question_id}", poll, name="poll")
11 | app.router.add_get("/poll/{question_id}/results", results, name="results")
12 | app.router.add_post("/poll/{question_id}/vote", vote, name="vote")
13 |
14 |
15 | def setup_static_routes(app):
16 | app.router.add_static("/static/", path=PROJECT_ROOT / "static", name="static")
17 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/settings.py:
--------------------------------------------------------------------------------
1 | # settings.py
2 | import argparse
3 | import pathlib
4 |
5 | from trafaret_config import commandline
6 |
7 | from aiohttpdemo_polls.utils import TRAFARET
8 |
9 |
10 | BASE_DIR = pathlib.Path(__file__).parent.parent
11 | DEFAULT_CONFIG_PATH = BASE_DIR / 'config' / 'polls.yaml'
12 |
13 |
14 | def get_config(argv=None):
15 | ap = argparse.ArgumentParser()
16 | commandline.standard_argparse_options(
17 | ap,
18 | default_config=DEFAULT_CONFIG_PATH
19 | )
20 |
21 | # ignore unknown options
22 | options, unknown = ap.parse_known_args(argv)
23 |
24 | config = commandline.config_from_options(options, TRAFARET)
25 | return config
26 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/static/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/polls/aiohttpdemo_polls/static/images/background.png
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/static/style.css:
--------------------------------------------------------------------------------
1 | li a {
2 | color: green;
3 | }
4 |
5 | body {
6 | background: white url("images/background.png") no-repeat;
7 | }
8 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% set title = "Page Not Found" %}
4 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% set title = "Internal Server Error" %}
4 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% block head %}
5 |
7 | {{title}}
8 | {% endblock %}
9 |
10 |
11 | {{title}}
12 |
13 | {% block content %}
14 | {% endblock %}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/templates/detail.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% set title = question.question_text %}
4 |
5 | {% block content %}
6 | {% if error_message %}{{ error_message }}
{% endif %}
7 |
8 |
9 | {% for choice in choices %}
10 |
11 | {{ choice.choice_text }}
12 | {% endfor %}
13 |
14 |
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% set title = "Main" %}
4 |
5 | {% block content %}
6 | {% if questions %}
7 |
12 | {% else %}
13 | No polls are available.
14 | {% endif %}
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/templates/results.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% set title = question.question_text %}
4 |
5 | {% block content %}
6 |
7 | {% for choice in choices %}
8 | {{ choice.choice_text }} -- {{ choice.votes }} vote(s)
9 | {% endfor %}
10 |
11 |
12 | Vote again?
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/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/polls/aiohttpdemo_polls/utils.py:
--------------------------------------------------------------------------------
1 | # utils.py
2 | import trafaret as T
3 |
4 |
5 | TRAFARET = T.Dict({
6 | T.Key('postgres'):
7 | T.Dict({
8 | 'database': T.String(),
9 | 'user': T.String(),
10 | 'password': T.String(),
11 | 'host': T.String(),
12 | 'port': T.Int(),
13 | 'minsize': T.Int(),
14 | 'maxsize': T.Int(),
15 | }),
16 | T.Key('host'): T.IP,
17 | T.Key('port'): T.Int(),
18 | })
19 |
--------------------------------------------------------------------------------
/demos/polls/aiohttpdemo_polls/views.py:
--------------------------------------------------------------------------------
1 | import aiohttp_jinja2
2 | from aiohttp import web
3 | from sqlalchemy import select
4 |
5 | from aiohttpdemo_polls import db
6 | from aiohttpdemo_polls.typedefs import db_key
7 |
8 |
9 | @aiohttp_jinja2.template("index.html")
10 | async def index(request):
11 | async with request.app[db_key]() as sess:
12 | questions = await sess.scalars(select(db.Question))
13 | return {"questions": questions.all()}
14 |
15 |
16 | @aiohttp_jinja2.template("detail.html")
17 | async def poll(request):
18 | async with request.app[db_key]() as sess:
19 | question_id = request.match_info["question_id"]
20 | try:
21 | question, choices = await db.get_question(sess, question_id)
22 | except db.RecordNotFound as e:
23 | raise web.HTTPNotFound(text=str(e))
24 | return {"question": question, "choices": choices}
25 |
26 |
27 | @aiohttp_jinja2.template("results.html")
28 | async def results(request):
29 | async with request.app[db_key]() as sess:
30 | question_id = int(request.match_info["question_id"])
31 |
32 | try:
33 | question, choices = await db.get_question(sess, question_id)
34 | except db.RecordNotFound as e:
35 | raise web.HTTPNotFound(text=str(e))
36 |
37 | return {"question": question, "choices": choices}
38 |
39 |
40 | async def vote(request):
41 | question_id = int(request.match_info["question_id"])
42 | data = await request.post()
43 | try:
44 | choice_id = int(data["choice"])
45 | except (KeyError, TypeError, ValueError) as e:
46 | raise web.HTTPBadRequest(text="You have not specified choice value") from e
47 | try:
48 | await db.vote(request.app, question_id, choice_id)
49 | except db.RecordNotFound as e:
50 | raise web.HTTPNotFound(text=str(e))
51 | router = request.app.router
52 | url = router["results"].url_for(question_id=str(question_id))
53 | raise web.HTTPFound(location=url)
54 |
--------------------------------------------------------------------------------
/demos/polls/config/polls.yaml:
--------------------------------------------------------------------------------
1 | postgres:
2 | database: aiohttpdemo_polls
3 | user: aiohttpdemo_user
4 | password: aiohttpdemo_pass
5 | host: localhost
6 | port: 5432
7 | minsize: 1
8 | maxsize: 5
9 |
10 | host: 127.0.0.1
11 | port: 8080
12 |
--------------------------------------------------------------------------------
/demos/polls/config/polls_test.yaml:
--------------------------------------------------------------------------------
1 | postgres:
2 | database: test_aiohttpdemo_polls
3 | user: test_aiohttpdemo_user
4 | password: aiohttpdemo_pass
5 | host: localhost
6 | port: 5432
7 | minsize: 1
8 | maxsize: 5
9 |
10 | host: 127.0.0.1
11 | port: 8080
12 |
--------------------------------------------------------------------------------
/demos/polls/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/polls/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.11.18
2 | aiohttp-jinja2==1.6
3 | asyncpg==0.30.0
4 | trafaret_config==2.0.2
5 | SQLalchemy==2.0.41
6 |
7 | # For testing
8 | pytest==8.3.5
9 | pytest-aiohttp==1.1.0
10 | flake8==7.2.0
11 |
--------------------------------------------------------------------------------
/demos/polls/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_polls', '__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_polls/__init__.py'
17 | raise RuntimeError(msg)
18 |
19 |
20 | install_requires = ['aiohttp',
21 | 'aiopg[sa]',
22 | 'aiohttp-jinja2',
23 | 'trafaret-config']
24 |
25 |
26 | setup(name='aiohttpdemo-polls',
27 | version=read_version(),
28 | description='Polls project example from aiohttp',
29 | platforms=['POSIX'],
30 | packages=find_packages(),
31 | package_data={
32 | '': ['templates/*.html', 'static/*.*']
33 | },
34 | include_package_data=True,
35 | install_requires=install_requires,
36 | zip_safe=False)
37 |
--------------------------------------------------------------------------------
/demos/polls/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/polls/tests/__init__.py
--------------------------------------------------------------------------------
/demos/polls/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from aiohttpdemo_polls.main import init_app
3 | from aiohttpdemo_polls.settings import BASE_DIR, get_config
4 | from init_db import create_tables, drop_tables, sample_data, setup_db, teardown_db
5 | from sqlalchemy.ext.asyncio import create_async_engine
6 |
7 | DSN = "postgresql+asyncpg://{user}:{password}@{host}:{port}/{database}"
8 | TEST_CONFIG_PATH = BASE_DIR / "config" / "polls_test.yaml"
9 | TEST_CONFIG = get_config(["-c", TEST_CONFIG_PATH.as_posix()])
10 | TEST_DB_URL = DSN.format(**TEST_CONFIG["postgres"])
11 |
12 |
13 | @pytest.fixture
14 | async def cli(aiohttp_client, db):
15 | app = await init_app(["-c", TEST_CONFIG_PATH.as_posix()])
16 | return await aiohttp_client(app)
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | async def db():
21 | test_config = get_config(["-c", TEST_CONFIG_PATH.as_posix()])
22 |
23 | await setup_db(test_config["postgres"])
24 | yield
25 | await teardown_db(test_config["postgres"])
26 |
27 |
28 | @pytest.fixture
29 | async def tables_and_data():
30 | test_engine = create_async_engine(TEST_DB_URL)
31 | await create_tables(test_engine)
32 | await sample_data(test_engine)
33 | yield
34 | await drop_tables(test_engine)
35 |
--------------------------------------------------------------------------------
/demos/polls/tests/test_integration.py:
--------------------------------------------------------------------------------
1 | """Require running database server"""
2 | from sqlalchemy import select
3 | from aiohttpdemo_polls.db import Choice
4 | from aiohttpdemo_polls.typedefs import db_key
5 |
6 |
7 | async def test_index(cli, tables_and_data):
8 | response = await cli.get('/')
9 | assert response.status == 200
10 | # TODO: resolve question with html code "'" instead of apostrophe in
11 | # assert 'What\'s new?' in await response.text()
12 | assert 'Main' in await response.text()
13 |
14 |
15 | async def test_results(cli, tables_and_data):
16 | response = await cli.get('/poll/1/results')
17 | assert response.status == 200
18 | assert 'Just hacking again' in await response.text()
19 |
20 |
21 | async def test_404_status(cli, tables_and_data):
22 | response = await cli.get('/no-such-route')
23 | assert response.status == 404
24 |
25 |
26 | async def test_vote(cli, tables_and_data):
27 |
28 | question_id = 1
29 | choice_text = 'Not much'
30 |
31 | async with cli.server.app[db_key].begin() as sess:
32 | result = await sess.scalars(
33 | select(Choice)
34 | .where(Choice.question_id == question_id)
35 | .where(Choice.choice_text == choice_text)
36 | )
37 | not_much_choice = result.first()
38 | not_much_choice_id = not_much_choice.id
39 | votes_before = not_much_choice.votes
40 |
41 | response = await cli.post(
42 | f'/poll/{question_id}/vote',
43 | data={'choice': not_much_choice_id}
44 | )
45 | assert response.status == 200
46 |
47 | async with cli.server.app[db_key].begin() as sess:
48 | result = await sess.scalars(
49 | select(Choice)
50 | .where(Choice.question_id == question_id)
51 | .where(Choice.choice_text == choice_text)
52 | )
53 | not_much_choice = result.first()
54 | votes_after = not_much_choice.votes
55 |
56 | assert votes_after == votes_before + 1
57 |
--------------------------------------------------------------------------------
/demos/polls/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py36
3 |
4 | [testenv]
5 | deps =
6 | pytest
7 | pytest-aiohttp
8 | usedevelop = True
9 | commands=py.test tests -s
10 |
--------------------------------------------------------------------------------
/demos/shortify/Makefile:
--------------------------------------------------------------------------------
1 | # Some simple testing tasks (sorry, UNIX only).
2 |
3 | FLAGS=
4 |
5 |
6 | flake:
7 | flake8 shortify setup.py
8 |
9 | clean:
10 | rm -rf `find . -name __pycache__`
11 | find . -type f -name '*.py[co]' -delete
12 | find . -type f -name '*~' -delete
13 | find . -type f -name '.*~' -delete
14 | find . -type f -name '@*' -delete
15 | find . -type f -name '#*#' -delete
16 | find . -type f -name '*.orig' -delete
17 | find . -type f -name '*.rej' -delete
18 | rm -f .coverage
19 | rm -rf coverage
20 | rm -rf build
21 | rm -rf htmlcov
22 | rm -rf dist
23 |
24 | run:
25 | python -m shortify
26 |
27 | docker_start_redis:
28 | docker-compose -f docker-compose.yml up -d redis
29 |
30 | docker_stop_redis:
31 | docker-compose -f docker-compose.yml stop redis
32 |
33 | .PHONY: flake clean
34 |
--------------------------------------------------------------------------------
/demos/shortify/README.rst:
--------------------------------------------------------------------------------
1 | Shortify Demo
2 | =============
3 |
4 | Install the app::
5 |
6 | $ cd demos/shortify
7 | $ pip install -e .
8 |
9 | Create database for your project::
10 |
11 | make docker_start_redis
12 |
13 |
14 | Run application::
15 |
16 | $ make run
17 |
18 | Open browser::
19 |
20 | http://127.0.0.1:9001
21 |
22 |
23 | Requirements
24 | ============
25 | * aiohttp_
26 | * aiohttp_jinja2_
27 |
28 |
29 | .. _Python: https://www.python.org
30 | .. _aiohttp: https://github.com/KeepSafe/aiohttp
31 | .. _motor: https://github.com/mongodb/motor
32 | .. _aiohttp_jinja2: https://github.com/aio-libs/aiohttp_jinja2
33 |
--------------------------------------------------------------------------------
/demos/shortify/config/config.yml:
--------------------------------------------------------------------------------
1 | redis:
2 | host: 127.0.0.1
3 | port: 6379
4 | db: 7
5 | minsize: 1
6 | maxsize: 5
7 |
8 | host: 127.0.0.1
9 | port: 9001
10 |
--------------------------------------------------------------------------------
/demos/shortify/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | redis:
3 | image: redis:6
4 | ports:
5 | - 6379:6379
6 |
--------------------------------------------------------------------------------
/demos/shortify/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 | asyncio_default_fixture_loop_scope = function
16 | filterwarnings =
17 | error
18 | testpaths = tests/
19 | xfail_strict = true
20 |
--------------------------------------------------------------------------------
/demos/shortify/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.11.18
2 | aiohttp-jinja2==1.6
3 | pyyaml==6.0.2
4 | redis==6.1.0
5 | trafaret==2.1.1
6 | pytest-aiohttp==1.1.0
7 | pytest-asyncio==0.26
8 |
--------------------------------------------------------------------------------
/demos/shortify/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 | 'shortify', '__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 shortify/__init__.py'
17 | raise RuntimeError(msg)
18 |
19 |
20 | install_requires = [
21 | 'aiohttp',
22 | 'trafaret',
23 | 'aiohttp_jinja2',
24 | 'pyyaml',
25 | 'redis>=4.2'
26 | ]
27 |
28 |
29 | setup(name='shortify',
30 | version=read_version(),
31 | description='Url shortener for aiohttp',
32 | platforms=['POSIX'],
33 | packages=find_packages(),
34 | include_package_data=True,
35 | install_requires=install_requires,
36 | zip_safe=False)
37 |
--------------------------------------------------------------------------------
/demos/shortify/shortify/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.0.1'
2 |
--------------------------------------------------------------------------------
/demos/shortify/shortify/__main__.py:
--------------------------------------------------------------------------------
1 | from shortify.main import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/demos/shortify/shortify/main.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | import pathlib
4 |
5 | import aiohttp_jinja2
6 | import jinja2
7 | from aiohttp import web
8 | from redis.asyncio import Redis
9 |
10 | from shortify.routes import setup_routes
11 | from shortify.utils import init_redis, load_config
12 | from shortify.views import SiteHandler
13 |
14 |
15 | PROJ_ROOT = pathlib.Path(__file__).parent.parent
16 | TEMPLATES_ROOT = pathlib.Path(__file__).parent / 'templates'
17 |
18 | # Define AppKey for Redis
19 | REDIS_KEY = web.AppKey("REDIS_KEY", Redis)
20 |
21 |
22 | async def setup_redis(app, conf):
23 | redis = await init_redis(conf["redis"])
24 | app[REDIS_KEY] = redis
25 | return redis
26 |
27 |
28 | def setup_jinja(app):
29 | loader = jinja2.FileSystemLoader(str(TEMPLATES_ROOT))
30 | jinja_env = aiohttp_jinja2.setup(app, loader=loader)
31 | return jinja_env
32 |
33 |
34 | async def init():
35 | conf = load_config(PROJ_ROOT / "config" / "config.yml")
36 |
37 | app = web.Application()
38 | redis = await setup_redis(app, conf)
39 | setup_jinja(app)
40 |
41 | handler = SiteHandler(redis, conf)
42 |
43 | setup_routes(app, handler, PROJ_ROOT)
44 | host, port = conf['host'], conf['port']
45 | return app, host, port
46 |
47 |
48 | async def get_app():
49 | """Used by aiohttp-devtools for local development."""
50 | import aiohttp_debugtoolbar
51 | app, _, _ = await init()
52 | aiohttp_debugtoolbar.setup(app)
53 | return app
54 |
55 |
56 | def main():
57 | logging.basicConfig(level=logging.DEBUG)
58 |
59 | loop = asyncio.get_event_loop()
60 | app, host, port = loop.run_until_complete(init())
61 | web.run_app(app, host=host, port=port)
62 |
63 |
64 | if __name__ == '__main__':
65 | main()
66 |
--------------------------------------------------------------------------------
/demos/shortify/shortify/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_get('/{short_id}', h.redirect, name='short')
6 | router.add_post('/shortify', h.shortify, name='shortify')
7 | router.add_static(
8 | '/static/', path=str(project_root / 'static'),
9 | name='static')
10 |
--------------------------------------------------------------------------------
/demos/shortify/shortify/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | shortify - aiohttp url shortener
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
37 |
38 |
39 |
40 |
44 |
45 |
50 |
51 |
52 | Shorten
53 |
54 |
55 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/demos/shortify/shortify/utils.py:
--------------------------------------------------------------------------------
1 | from redis import asyncio as aioredis
2 | import trafaret as t
3 | import yaml
4 | from aiohttp import web
5 |
6 |
7 | CONFIG_TRAFARET = t.Dict(
8 | {
9 | t.Key('redis'): t.Dict(
10 | {
11 | 'port': t.Int(),
12 | 'host': t.String(),
13 | 'db': t.Int(),
14 | 'minsize': t.Int(),
15 | 'maxsize': t.Int(),
16 | }
17 | ),
18 | 'host': t.IP,
19 | 'port': t.Int(),
20 | }
21 | )
22 |
23 |
24 | def load_config(fname):
25 | with open(fname, 'rt') as f:
26 | data = yaml.safe_load(f)
27 | return CONFIG_TRAFARET.check(data)
28 |
29 |
30 | async def init_redis(conf):
31 | redis = await aioredis.from_url(
32 | f"redis://{conf['host']}:{conf['port']}",
33 | )
34 | return redis
35 |
36 |
37 | CHARS = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
38 |
39 |
40 | def encode(num, alphabet=CHARS):
41 | if num == 0:
42 | return alphabet[0]
43 | arr = []
44 | base = len(alphabet)
45 | while num:
46 | num, rem = divmod(num, base)
47 | arr.append(alphabet[rem])
48 | arr.reverse()
49 | return ''.join(arr)
50 |
51 |
52 | ShortifyRequest = t.Dict({t.Key('url'): t.URL})
53 |
54 |
55 | def fetch_url(data):
56 | try:
57 | data = ShortifyRequest(data)
58 | except t.DataError:
59 | raise web.HTTPBadRequest('URL is not valid')
60 | return data['url']
61 |
--------------------------------------------------------------------------------
/demos/shortify/shortify/views.py:
--------------------------------------------------------------------------------
1 | import aiohttp_jinja2
2 | from aiohttp import web
3 |
4 | from .utils import encode, fetch_url
5 |
6 |
7 | class SiteHandler:
8 |
9 | def __init__(self, redis, conf):
10 | self._redis = redis
11 | self._conf = conf
12 |
13 | @aiohttp_jinja2.template('index.html')
14 | async def index(self, request):
15 | return {}
16 |
17 | async def shortify(self, request):
18 | data = await request.json()
19 | long_url = fetch_url(data)
20 |
21 | index = await self._redis.incr("shortify:count")
22 | path = encode(index - 1)
23 | key = "shortify:{}".format(path)
24 | await self._redis.set(key, long_url)
25 |
26 | url = "http://{host}:{port}/{path}".format(
27 | host=self._conf['host'],
28 | port=self._conf['port'],
29 | path=path)
30 |
31 | return web.json_response({"url": url})
32 |
33 | async def redirect(self, request):
34 | short_id = request.match_info['short_id']
35 | key = 'shortify:{}'.format(short_id)
36 | location = await self._redis.get(key)
37 | if not location:
38 | raise web.HTTPNotFound()
39 | raise web.HTTPFound(location=location.decode())
40 |
--------------------------------------------------------------------------------
/demos/shortify/static/css/custom.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | }
4 |
5 | html, body {
6 | height: 100%;
7 | }
8 |
9 | #content {
10 | margin-top: 20px;
11 | margin-bottom: 60px;
12 | }
13 |
14 | header {
15 | margin-bottom: 30px;
16 | padding-bottom: 10px;
17 | clear: both;
18 | }
19 |
20 | #wrap {
21 | margin: 5px 10px -50px 10px;
22 | padding: 10px;
23 | text-align: center;
24 | min-height: 100%;
25 | }
26 | #url-result {
27 | color: #039be5;
28 | display: hidden;
29 | }
30 |
31 | .btn {
32 | float: right;
33 | }
34 |
35 | footer {
36 | color: #039be5;
37 | text-align: center;
38 | height: 50px;
39 | line-height: 50px;
40 | }
41 |
42 | footer a{
43 | color: #039be5;
44 | }
45 |
--------------------------------------------------------------------------------
/demos/shortify/static/font/roboto/Roboto-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/shortify/static/font/roboto/Roboto-Regular.woff
--------------------------------------------------------------------------------
/demos/shortify/static/font/roboto/Roboto-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/shortify/static/font/roboto/Roboto-Regular.woff2
--------------------------------------------------------------------------------
/demos/shortify/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/demos/shortify/tests/__init__.py
--------------------------------------------------------------------------------
/demos/shortify/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import pytest
4 | from redis import asyncio as aioredis
5 |
6 | from shortify.main import init, PROJ_ROOT
7 | from shortify.utils import load_config
8 |
9 | TEST_CONFIG_PATH = PROJ_ROOT / "config" / "config.yml"
10 | TEST_CONFIG = load_config(TEST_CONFIG_PATH.as_posix())
11 |
12 |
13 | @pytest.fixture
14 | async def cli(aiohttp_client):
15 | app, _, _ = await init()
16 | return await aiohttp_client(app)
17 |
18 |
19 | @pytest.fixture
20 | async def redis():
21 | """Create a Redis connection for testing."""
22 | redis_config = TEST_CONFIG["redis"]
23 | redis = await aioredis.from_url(
24 | f"redis://{redis_config['host']}:{redis_config['port']}",
25 | )
26 | yield redis
27 | await redis.aclose()
28 | # Give time for all connections to close properly
29 | await asyncio.sleep(0.1)
30 |
31 |
32 | @pytest.fixture(autouse=True)
33 | async def clean_redis(redis):
34 | """Clean Redis database before each test."""
35 | await redis.flushdb()
36 | # Give time for all connections to close properly
37 | await asyncio.sleep(0.1)
38 |
--------------------------------------------------------------------------------
/demos/shortify/tests/test_shortify.py:
--------------------------------------------------------------------------------
1 | async def test_create_short_url(cli, clean_redis):
2 | """Test creating a short URL for a long URL."""
3 | long_url = "https://example.com/very/long/url/that/needs/shortening"
4 |
5 | async with cli.post("/shortify", json={"url": long_url}) as resp:
6 | await resp.read()
7 | assert resp.status == 200
8 |
9 | data = await resp.json()
10 | assert "url" in data
11 | assert data["url"] == "http://127.0.0.1:9001/a"
12 |
13 |
14 | async def test_redirect_to_long_url(cli, clean_redis):
15 | """Test redirecting from short URL to original long URL."""
16 | long_url = "https://example.com/very/long/url/that/needs/shortening"
17 |
18 | # Create a short URL
19 | async with cli.post("/shortify", json={"url": long_url}) as resp:
20 | await resp.read()
21 | assert resp.status == 200
22 | data = await resp.json()
23 | short_url = data["url"]
24 | short_code = short_url.split("/")[-1]
25 |
26 | # Test the redirect
27 | async with cli.get(f"/{short_code}", allow_redirects=False) as resp:
28 | await resp.read()
29 | assert resp.status == 302
30 | assert resp.headers["Location"] == long_url
31 |
32 |
33 | async def test_invalid_short_url(cli, clean_redis):
34 | """Test accessing a non-existent short URL."""
35 | async with cli.get("/nonexistent", allow_redirects=False) as resp:
36 | await resp.read()
37 | assert resp.status == 404
38 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = aiohttp-demos
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/_static/aiohttp-icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/aiohttp-icon-128x128.png
--------------------------------------------------------------------------------
/docs/_static/aiohttp-icon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/aiohttp-icon-32x32.png
--------------------------------------------------------------------------------
/docs/_static/aiohttp-icon-64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/aiohttp-icon-64x64.png
--------------------------------------------------------------------------------
/docs/_static/aiohttp-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/aiohttp-icon-96x96.png
--------------------------------------------------------------------------------
/docs/_static/blog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/blog.png
--------------------------------------------------------------------------------
/docs/_static/chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/chat.png
--------------------------------------------------------------------------------
/docs/_static/graph.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/graph.gif
--------------------------------------------------------------------------------
/docs/_static/imagetagger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/imagetagger.png
--------------------------------------------------------------------------------
/docs/_static/moderator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/moderator.png
--------------------------------------------------------------------------------
/docs/_static/motortwit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/motortwit.png
--------------------------------------------------------------------------------
/docs/_static/polls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/polls.png
--------------------------------------------------------------------------------
/docs/_static/shorty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/shorty.png
--------------------------------------------------------------------------------
/docs/_static/slack_moderator.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/_static/slack_moderator.gif
--------------------------------------------------------------------------------
/docs/aiohttp-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aio-libs/aiohttp-demos/acbc750236429b342affa6b40c23456cc65e7634/docs/aiohttp-icon.ico
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. aiohttp-demos documentation master file, created by
2 | sphinx-quickstart on Sun Oct 29 12:51:35 2017.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to aiohttp-demos
7 | ========================
8 |
9 | .. _aiohttp-demos-polls-beginning:
10 |
11 | If you want to create an application with *aiohttp* there is a step-by-step guide
12 | for *Polls* application (:ref:`aiohttp-demos-polls-getting-started`).
13 | The application is similar to the one from Django tutorial. It allows people to create polls and vote.
14 |
15 | There are also many other demo projects, give them a try!
16 |
17 |
18 | Example Projects
19 | ================
20 |
21 | We have created for you a selection of fun projects, that can show you how to
22 | create application from the *blog* to the applications related to data science.
23 | Please feel free to add your open source example project by making Pull
24 | Request.
25 |
26 | - `Shortify `_
27 | - *URL shortener* with *Redis* storage.
28 |
29 | - `Moderator `_
30 | - UI and API for classification of offensive and toxic comments using
31 | *Kaggle* data and *scikit-learn*.
32 |
33 | - `Moderator bot `_
34 | - Slack bot for moderating offensive and toxic comments using provided model from *Moderator AI*
35 |
36 | - `Motortwit `_
37 | - *Twitter* clone with *MongoDB* storage.
38 |
39 | - `Imagetagger `_
40 | - Example how to deploy deep learning model with *aiohttp*.
41 |
42 | - `Chat `_
43 | - Simple *chat* using websockets.
44 |
45 | - `Polls `_
46 | - Simple *polls* application with PostgreSQL storage.
47 |
48 | - `Blog `_
49 | - The *blog* application with *PostgreSQL* storage and *Redis* session store.
50 |
51 | - `Graphql `_
52 | - The simple real-time chat that based on the *GraphQL* api and *Apollo client*.
53 |
54 |
55 | -------------------------------------------------------------------------
56 |
57 | Contents
58 | ========
59 |
60 | .. toctree::
61 | :maxdepth: 2
62 |
63 | preparations
64 | tutorial
65 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=aiohttp-demos
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
20 | echo.installed, then set the SPHINXBUILD environment variable to point
21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
22 | echo.may add the Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | # Workaround for https://github.com/dependabot/dependabot-core/issues/2178
2 | -r demos/blog/requirements.txt
3 | -r demos/chat/requirements.txt
4 | -r demos/graphql-demo/requirements-dev.txt
5 | -r demos/imagetagger/requirements.txt
6 | -r demos/moderator/requirements-dev.txt
7 | -r demos/moderator_bot/requirements-dev.txt
8 | -r demos/motortwit/requirements.txt
9 | -r demos/polls/requirements-dev.txt
10 | -r demos/shortify/requirements.txt
11 |
12 | # lint
13 | flake8==7.2.0
14 | flake8-bugbear==24.12.12
15 | flake8-quotes==3.4.0
16 |
17 | # test
18 | pytest==8.3.5
19 | pytest-aiohttp==1.1.0
20 |
21 |
22 | # dev
23 | psycopg2==2.9.10
24 |
25 | # docs
26 | pygments==2.19.1
27 | sphinx==8.2.3
28 | sphinxcontrib-asyncio==0.3.0
29 | sphinxcontrib-spelling==8.0.1; platform_system!="Windows" # We only use it in CI
30 |
--------------------------------------------------------------------------------