├── tests ├── __init__.py ├── test_send_messages_to_cp.py ├── central_system.py └── test_send_messages_to_central_system.py ├── ocpp_simulator ├── cp_management │ ├── __init__.py │ └── cp.py ├── __init__.py └── cli.py ├── README.rst ├── .github └── workflows │ ├── pypi.yaml │ └── push.yaml ├── pyproject.toml ├── LICENSE ├── .prospector.yaml ├── makefile ├── .gitignore └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ocpp_simulator/cp_management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ocpp_simulator/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | OCPP simulator 3 | ======= 4 | 5 | Introduction 6 | ============ 7 | 8 | This simulator is based on `OCPP `_ which is used to simulate a charge point. 9 | 10 | 11 | Getting Started 12 | =============== 13 | 14 | 15 | Installation 16 | ------------ 17 | 18 | 19 | How Does it Work? 20 | ----------------- 21 | 22 | 23 | Example 24 | ------- 25 | 26 | 27 | License 28 | ======= 29 | 30 | This project is licensed under the terms of the MIT license. 31 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to pypi 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | persist-credentials: false 15 | - uses: actions/setup-python@v2 16 | with: 17 | python-version: '3.9' 18 | - name: Install poetry 19 | run: | 20 | pip install poetry 21 | - name: Publish 22 | run: | 23 | poetry publish --build --username ${{ secrets.PYPI_USER }} --password ${{ secrets.PYPI_PASSWORD }} 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ocpp-simulator" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["ViCt0r99 "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | typer = "^0.6.1" 10 | questionary = "^1.10.0" 11 | websockets = "^10.3" 12 | ocpp = "^0.15.0" 13 | Faker = "^13.15.1" 14 | factory-boy = "^3.2.1" 15 | pytest = "^7.1.2" 16 | pytest-asyncio = "^0.19.0" 17 | 18 | [tool.poetry.dev-dependencies] 19 | pytest = "^7.1.2" 20 | pytest-cov = "^2.10.1" 21 | prospector = "^1.7.7" 22 | bandit = "^1.7.4" 23 | 24 | [build-system] 25 | requires = ["poetry-core>=1.0.0"] 26 | build-backend = "poetry.core.masonry.api" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 TECHS-Technological-Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.prospector.yaml: -------------------------------------------------------------------------------- 1 | inherits: 2 | - strictness_veryhigh 3 | - full_pep8 4 | 5 | ignore-paths: 6 | - bin 7 | - config 8 | - docs 9 | - coverage 10 | - requirements 11 | - venv 12 | - env 13 | - node_modules 14 | - manage.py 15 | - alembic 16 | 17 | doc-warnings: false 18 | test-warnings: true 19 | 20 | uses: 21 | - celery 22 | 23 | pylint: 24 | disable: 25 | - wildcard-import 26 | - relative-import 27 | - invalid-name 28 | - unused-wildcard-import 29 | - wrong-import-position 30 | - too-few-public-methods 31 | - old-style-class 32 | - no-init 33 | - no-self-use 34 | - unused-argument 35 | - too-many-arguments 36 | - too-many-instance-attributes 37 | - attribute-defined-outside-init 38 | - redefined-builtin 39 | - too-many-ancestors 40 | - arguments-differ 41 | - abstract-method 42 | - too-many-function-args 43 | - assignment-from-none 44 | - redefined-outer-name 45 | - no-self-argument 46 | options: 47 | max-locals: 25 48 | max-line-length: 120 49 | 50 | pycodestyle: 51 | disable: 52 | - E402 53 | - N805 54 | options: 55 | max-line-length: 120 56 | 57 | pyflakes: 58 | disable: 59 | - F403 60 | - F401 61 | - F999 -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: CI tests on push 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'develop' 7 | - 'feature/**' 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | persist-credentials: false 16 | - uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.9' 19 | - name: Setup poetry 20 | run: | 21 | sudo -H pip install -U poetry 22 | - name: Install packages 23 | run: | 24 | make install 25 | - name: Run linter 26 | run: | 27 | poetry run prospector ./ocpp_simulator --profile .prospector.yaml 28 | test: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | with: 33 | persist-credentials: false 34 | - uses: actions/setup-python@v2 35 | with: 36 | python-version: '3.9' 37 | - name: Setup poetry 38 | run: | 39 | sudo -H pip install -U poetry 40 | - name: Install packages 41 | run: | 42 | make install 43 | - name: Run test suite 44 | run: | 45 | poetry run pytest -v 46 | security: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | with: 51 | persist-credentials: false 52 | - uses: actions/setup-python@v2 53 | with: 54 | python-version: '3.9' 55 | - name: Setup poetry 56 | run: | 57 | sudo -H pip install -U poetry 58 | - name: Install packages 59 | run: | 60 | make install 61 | - name: Run bandit 62 | run: | 63 | poetry run bandit -r ./ocpp_simulator 64 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help .check-pypi-envs .install-poetry update install tests build deploy 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | export PATH := ${HOME}/.local/bin:$(PATH) 6 | 7 | IS_POETRY := $(shell pip freeze | grep "poetry==") 8 | 9 | CURRENT_VERSION := $(shell poetry version -s) 10 | 11 | help: 12 | @echo "Please use 'make ', where is one of" 13 | @echo "" 14 | @echo " build builds the project .whl with poetry" 15 | @echo " deploy deploys the project using poetry (not recommended, only use if really needed)" 16 | @echo " help outputs this helper" 17 | @echo " install installs the dependencies in the env" 18 | @echo " release version= bumps the project version to , using poetry;" 19 | @echo " Updates also docs/source/conf.py version;" 20 | @echo " If no version is provided, poetry outputs the current project version" 21 | @echo " test run all the tests and linting" 22 | @echo " update updates the dependencies in poetry.lock" 23 | @echo "" 24 | @echo "Check the Makefile to know exactly what each target is doing." 25 | 26 | 27 | .install-poetry: 28 | @if [ -z ${IS_POETRY} ]; then pip install poetry; fi 29 | 30 | update: .install-poetry 31 | poetry update 32 | 33 | install: .install-poetry 34 | poetry install 35 | 36 | tests: .install-poetry 37 | poetry run flake8 ocpp tests 38 | poetry run py.test -vvv --cov=ocpp --cov-report=term-missing tests/ 39 | 40 | build: .install-poetry 41 | poetry build 42 | 43 | release: .install-poetry 44 | @echo "Please remember to update the CHANGELOG.md, before tagging the release" 45 | @sed -i ".bkp" "s/release = '${CURRENT_VERSION}'/release = '${version}'/g" docs/source/conf.py 46 | @poetry version ${version} 47 | 48 | deploy: update tests 49 | poetry publish --build -------------------------------------------------------------------------------- /ocpp_simulator/cli.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import typer 3 | import questionary 4 | import websockets 5 | 6 | from cp_management import cp as Cp # noqa 7 | 8 | app = typer.Typer() 9 | 10 | 11 | async def connect_cp_to_central_system(url_websocket_address: str, cp_serial_number: str) -> Cp.ChargePoint: 12 | # Connect to central system 13 | ws = await websockets.connect( 14 | f'ws://{url_websocket_address}/{cp_serial_number}', 15 | subprotocols=['ocpp2.0.1'] 16 | ) 17 | cp = Cp.ChargePoint(cp_serial_number, ws) 18 | loop = asyncio.get_event_loop() 19 | loop.create_task(cp.start()) 20 | 21 | # Boot notification 22 | typer.secho('Boot notification', fg=typer.colors.BRIGHT_GREEN, bold=True) 23 | message = await cp.send_boot_notification() 24 | typer.echo(message) 25 | 26 | # Status notification 27 | typer.secho('Status notification', fg=typer.colors.BRIGHT_GREEN, bold=True) 28 | message = await cp.send_status_notification() 29 | typer.echo(message) 30 | 31 | return cp 32 | 33 | 34 | async def send_message(cp, message: str): 35 | response = await cp.messages[message](cp) 36 | typer.echo(response) 37 | 38 | 39 | @app.command() 40 | def start(): 41 | async def _start(): 42 | # Program initialization 43 | program_continue = typer.confirm("You have just started Charge Point simulator, do you want to continue") 44 | if not program_continue: 45 | raise typer.Abort() 46 | 47 | # Central system and charge point information 48 | cp_serial_number = typer.prompt("Enter charge point serial number") 49 | central_system_url = typer.prompt("Enter central system URL") 50 | 51 | # Connect to charge point 52 | cp = await connect_cp_to_central_system(central_system_url, cp_serial_number) 53 | 54 | answer = await questionary.select( 55 | "What action do you want to perform: ", 56 | choices=[ 57 | 'Send an OCPP message', 58 | 'Quit' 59 | ] 60 | ).ask_async() 61 | 62 | if answer == 'Quit': 63 | raise typer.Abort() 64 | 65 | # Sending messages to particular charge point 66 | while True: 67 | message = await questionary.select( 68 | "What message do you want to send: ", 69 | choices=list(cp.messages.keys()) 70 | ).ask_async() 71 | 72 | await send_message(cp, message) 73 | 74 | stop_sending_messages = typer.confirm("Do you want to send another message?") 75 | if not stop_sending_messages: 76 | break 77 | 78 | # Make Typer command running in async version 79 | asyncio.run(_start()) 80 | 81 | 82 | if __name__ == '__main__': 83 | typer.run(start) 84 | -------------------------------------------------------------------------------- /.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 | .idea/ 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 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | # Text Editor 142 | .vscode 143 | -------------------------------------------------------------------------------- /tests/test_send_messages_to_cp.py: -------------------------------------------------------------------------------- 1 | import websockets 2 | import pytest 3 | import asyncio 4 | 5 | from faker import Faker 6 | 7 | from .central_system import ChargePoint as central_system 8 | from ocpp_simulator.cp_management import cp as Cp 9 | 10 | 11 | fake = Faker() 12 | 13 | 14 | async def cancel_tasks(): 15 | for task in asyncio.all_tasks(): 16 | task.cancel() 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_cancel_reservation(): 21 | server = await Cp.start_cp() 22 | async with websockets.connect( 23 | f'ws://0.0.0.0:9000/123', 24 | subprotocols=['ocpp2.0.1'] 25 | ) as ws: 26 | cp = central_system('123', ws) 27 | loop = asyncio.get_running_loop() 28 | loop.create_task(cp.start()) 29 | 30 | result = await cp.send_cancel_reservation() 31 | assert result.status == 'Accepted' 32 | 33 | await cancel_tasks() 34 | server.close() 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_certificate_signed(): 39 | server = await Cp.start_cp() 40 | async with websockets.connect( 41 | f'ws://0.0.0.0:9000/123', 42 | subprotocols=['ocpp2.0.1'] 43 | ) as ws: 44 | cp = central_system('123', ws) 45 | loop = asyncio.get_running_loop() 46 | loop.create_task(cp.start()) 47 | 48 | result = await cp.send_certificate_signed() 49 | assert result.status == 'Accepted' 50 | 51 | await cancel_tasks() 52 | server.close() 53 | 54 | 55 | @pytest.mark.asyncio 56 | async def test_change_availability(): 57 | server = await Cp.start_cp() 58 | async with websockets.connect( 59 | f'ws://0.0.0.0:9000/123', 60 | subprotocols=['ocpp2.0.1'] 61 | ) as ws: 62 | cp = central_system('123', ws) 63 | loop = asyncio.get_running_loop() 64 | loop.create_task(cp.start()) 65 | 66 | result = await cp.send_change_availability() 67 | assert result.status == 'Accepted' 68 | 69 | await cancel_tasks() 70 | server.close() 71 | 72 | 73 | @pytest.mark.asyncio 74 | async def test_clear_charging_profile(): 75 | server = await Cp.start_cp() 76 | async with websockets.connect( 77 | f'ws://0.0.0.0:9000/123', 78 | subprotocols=['ocpp2.0.1'] 79 | ) as ws: 80 | cp = central_system('123', ws) 81 | loop = asyncio.get_running_loop() 82 | loop.create_task(cp.start()) 83 | 84 | result = await cp.send_clear_charging_profile() 85 | assert result.status == 'Accepted' 86 | 87 | await cancel_tasks() 88 | server.close() 89 | 90 | 91 | @pytest.mark.asyncio 92 | async def test_clear_display_message(): 93 | server = await Cp.start_cp() 94 | async with websockets.connect( 95 | f'ws://0.0.0.0:9000/123', 96 | subprotocols=['ocpp2.0.1'] 97 | ) as ws: 98 | cp = central_system('123', ws) 99 | loop = asyncio.get_running_loop() 100 | loop.create_task(cp.start()) 101 | 102 | result = await cp.send_clear_display_message() 103 | assert result.status == 'Accepted' 104 | 105 | await cancel_tasks() 106 | server.close() 107 | 108 | 109 | @pytest.mark.asyncio 110 | async def test_clear_variable_monitoring(): 111 | server = await Cp.start_cp() 112 | async with websockets.connect( 113 | f'ws://0.0.0.0:9000/123', 114 | subprotocols=['ocpp2.0.1'] 115 | ) as ws: 116 | cp = central_system('123', ws) 117 | loop = asyncio.get_running_loop() 118 | loop.create_task(cp.start()) 119 | 120 | result = await cp.send_clear_variable_monitoring() 121 | assert result.clear_monitoring_result[0]['status'] == 'Accepted' 122 | 123 | await cancel_tasks() 124 | server.close() 125 | 126 | 127 | @pytest.mark.asyncio 128 | async def test_cost_updated(): 129 | server = await Cp.start_cp() 130 | async with websockets.connect( 131 | f'ws://0.0.0.0:9000/123', 132 | subprotocols=['ocpp2.0.1'] 133 | ) as ws: 134 | cp = central_system('123', ws) 135 | loop = asyncio.get_running_loop() 136 | loop.create_task(cp.start()) 137 | 138 | result = await cp.send_cost_updated() 139 | assert str(result) == 'CostUpdatedPayload()' 140 | 141 | await cancel_tasks() 142 | server.close() 143 | 144 | 145 | @pytest.mark.asyncio 146 | async def test_customer_info(): 147 | server = await Cp.start_cp() 148 | async with websockets.connect( 149 | f'ws://0.0.0.0:9000/123', 150 | subprotocols=['ocpp2.0.1'] 151 | ) as ws: 152 | cp = central_system('123', ws) 153 | loop = asyncio.get_running_loop() 154 | loop.create_task(cp.start()) 155 | 156 | result = await cp.send_customer_info() 157 | assert result.status == 'Accepted' 158 | 159 | await cancel_tasks() 160 | server.close() 161 | 162 | 163 | @pytest.mark.asyncio 164 | async def test_data_transfer(): 165 | server = await Cp.start_cp() 166 | async with websockets.connect( 167 | f'ws://0.0.0.0:9000/123', 168 | subprotocols=['ocpp2.0.1'] 169 | ) as ws: 170 | cp = central_system('123', ws) 171 | loop = asyncio.get_running_loop() 172 | loop.create_task(cp.start()) 173 | 174 | result = await cp.send_data_transfer() 175 | assert result.status == 'Accepted' 176 | 177 | await cancel_tasks() 178 | server.close() 179 | 180 | 181 | @pytest.mark.asyncio 182 | async def test_delete_certificate(): 183 | server = await Cp.start_cp() 184 | async with websockets.connect( 185 | f'ws://0.0.0.0:9000/123', 186 | subprotocols=['ocpp2.0.1'] 187 | ) as ws: 188 | cp = central_system('123', ws) 189 | loop = asyncio.get_running_loop() 190 | loop.create_task(cp.start()) 191 | 192 | result = await cp.send_delete_certificate() 193 | assert result.status == 'Accepted' 194 | 195 | await cancel_tasks() 196 | server.close() 197 | 198 | 199 | @pytest.mark.asyncio 200 | async def test_get_base_report(): 201 | server = await Cp.start_cp() 202 | async with websockets.connect( 203 | f'ws://0.0.0.0:9000/123', 204 | subprotocols=['ocpp2.0.1'] 205 | ) as ws: 206 | cp = central_system('123', ws) 207 | loop = asyncio.get_running_loop() 208 | loop.create_task(cp.start()) 209 | 210 | result = await cp.send_get_base_report() 211 | assert result.status == 'Accepted' 212 | 213 | await cancel_tasks() 214 | server.close() 215 | 216 | 217 | @pytest.mark.asyncio 218 | async def test_get_charging_profiles(): 219 | server = await Cp.start_cp() 220 | async with websockets.connect( 221 | f'ws://0.0.0.0:9000/123', 222 | subprotocols=['ocpp2.0.1'] 223 | ) as ws: 224 | cp = central_system('123', ws) 225 | loop = asyncio.get_running_loop() 226 | loop.create_task(cp.start()) 227 | 228 | result = await cp.send_get_charging_profiles() 229 | assert result.status == 'Accepted' 230 | 231 | await cancel_tasks() 232 | server.close() 233 | 234 | 235 | @pytest.mark.asyncio 236 | async def test_get_composite_schedule(): 237 | server = await Cp.start_cp() 238 | async with websockets.connect( 239 | f'ws://0.0.0.0:9000/123', 240 | subprotocols=['ocpp2.0.1'] 241 | ) as ws: 242 | cp = central_system('123', ws) 243 | loop = asyncio.get_running_loop() 244 | loop.create_task(cp.start()) 245 | 246 | result = await cp.send_get_composite_schedule() 247 | assert result.status == 'Accepted' 248 | 249 | await cancel_tasks() 250 | server.close() 251 | 252 | 253 | @pytest.mark.asyncio 254 | async def test_get_display_messages(): 255 | server = await Cp.start_cp() 256 | async with websockets.connect( 257 | f'ws://0.0.0.0:9000/123', 258 | subprotocols=['ocpp2.0.1'] 259 | ) as ws: 260 | cp = central_system('123', ws) 261 | loop = asyncio.get_running_loop() 262 | loop.create_task(cp.start()) 263 | 264 | result = await cp.send_get_display_messages() 265 | assert result.status == 'Accepted' 266 | 267 | await cancel_tasks() 268 | server.close() 269 | 270 | 271 | @pytest.mark.asyncio 272 | async def test_get_installed_certificate_ids(): 273 | server = await Cp.start_cp() 274 | async with websockets.connect( 275 | f'ws://0.0.0.0:9000/123', 276 | subprotocols=['ocpp2.0.1'] 277 | ) as ws: 278 | cp = central_system('123', ws) 279 | loop = asyncio.get_running_loop() 280 | loop.create_task(cp.start()) 281 | 282 | result = await cp.send_get_installed_certificate_ids() 283 | assert result.status == 'Accepted' 284 | 285 | await cancel_tasks() 286 | server.close() 287 | 288 | 289 | @pytest.mark.asyncio 290 | async def test_get_local_list_version(): 291 | server = await Cp.start_cp() 292 | async with websockets.connect( 293 | f'ws://0.0.0.0:9000/123', 294 | subprotocols=['ocpp2.0.1'] 295 | ) as ws: 296 | cp = central_system('123', ws) 297 | loop = asyncio.get_running_loop() 298 | loop.create_task(cp.start()) 299 | 300 | result = await cp.send_get_local_list_version() 301 | assert 'GetLocalListVersionPayload' in str(result) 302 | 303 | await cancel_tasks() 304 | server.close() 305 | 306 | 307 | @pytest.mark.asyncio 308 | async def test_get_log(): 309 | server = await Cp.start_cp() 310 | async with websockets.connect( 311 | f'ws://0.0.0.0:9000/123', 312 | subprotocols=['ocpp2.0.1'] 313 | ) as ws: 314 | cp = central_system('123', ws) 315 | loop = asyncio.get_running_loop() 316 | loop.create_task(cp.start()) 317 | 318 | result = await cp.send_get_log() 319 | assert result.status == 'Accepted' 320 | 321 | await cancel_tasks() 322 | server.close() 323 | 324 | 325 | @pytest.mark.asyncio 326 | async def test_get_monitoring_report(): 327 | server = await Cp.start_cp() 328 | async with websockets.connect( 329 | f'ws://0.0.0.0:9000/123', 330 | subprotocols=['ocpp2.0.1'] 331 | ) as ws: 332 | cp = central_system('123', ws) 333 | loop = asyncio.get_running_loop() 334 | loop.create_task(cp.start()) 335 | 336 | result = await cp.send_get_monitoring_report() 337 | assert result.status == 'Accepted' 338 | 339 | await cancel_tasks() 340 | server.close() 341 | 342 | 343 | @pytest.mark.asyncio 344 | async def test_publish_firmware(): 345 | server = await Cp.start_cp() 346 | async with websockets.connect( 347 | f'ws://0.0.0.0:9000/123', 348 | subprotocols=['ocpp2.0.1'] 349 | ) as ws: 350 | cp = central_system('123', ws) 351 | loop = asyncio.get_running_loop() 352 | loop.create_task(cp.start()) 353 | 354 | result = await cp.send_publish_firmware() 355 | assert result.status == 'Accepted' 356 | 357 | await cancel_tasks() 358 | server.close() 359 | 360 | 361 | @pytest.mark.asyncio 362 | async def test_reserve_now(): 363 | server = await Cp.start_cp() 364 | async with websockets.connect( 365 | f'ws://0.0.0.0:9000/123', 366 | subprotocols=['ocpp2.0.1'] 367 | ) as ws: 368 | cp = central_system('123', ws) 369 | loop = asyncio.get_running_loop() 370 | loop.create_task(cp.start()) 371 | 372 | result = await cp.send_reserve_now() 373 | assert result.status == 'Accepted' 374 | 375 | await cancel_tasks() 376 | server.close() 377 | 378 | 379 | @pytest.mark.asyncio 380 | async def test_reset(): 381 | server = await Cp.start_cp() 382 | async with websockets.connect( 383 | f'ws://0.0.0.0:9000/123', 384 | subprotocols=['ocpp2.0.1'] 385 | ) as ws: 386 | cp = central_system('123', ws) 387 | loop = asyncio.get_running_loop() 388 | loop.create_task(cp.start()) 389 | 390 | result = await cp.send_reset() 391 | assert result.status == 'Accepted' 392 | 393 | await cancel_tasks() 394 | server.close() 395 | 396 | 397 | @pytest.mark.asyncio 398 | async def test_send_local_list(): 399 | server = await Cp.start_cp() 400 | async with websockets.connect( 401 | f'ws://0.0.0.0:9000/123', 402 | subprotocols=['ocpp2.0.1'] 403 | ) as ws: 404 | cp = central_system('123', ws) 405 | loop = asyncio.get_running_loop() 406 | loop.create_task(cp.start()) 407 | 408 | result = await cp.send_send_local_list() 409 | assert result.status == 'Accepted' 410 | 411 | await cancel_tasks() 412 | server.close() 413 | 414 | 415 | @pytest.mark.asyncio 416 | async def test_set_charging_profile(): 417 | server = await Cp.start_cp() 418 | async with websockets.connect( 419 | f'ws://0.0.0.0:9000/123', 420 | subprotocols=['ocpp2.0.1'] 421 | ) as ws: 422 | cp = central_system('123', ws) 423 | loop = asyncio.get_running_loop() 424 | loop.create_task(cp.start()) 425 | 426 | result = await cp.send_set_charging_profile() 427 | assert result.status == 'Accepted' 428 | 429 | await cancel_tasks() 430 | server.close() 431 | 432 | 433 | @pytest.mark.asyncio 434 | async def test_set_display_message(): 435 | server = await Cp.start_cp() 436 | async with websockets.connect( 437 | f'ws://0.0.0.0:9000/123', 438 | subprotocols=['ocpp2.0.1'] 439 | ) as ws: 440 | cp = central_system('123', ws) 441 | loop = asyncio.get_running_loop() 442 | loop.create_task(cp.start()) 443 | 444 | result = await cp.send_set_display_message() 445 | assert result.status == 'Accepted' 446 | 447 | await cancel_tasks() 448 | server.close() 449 | 450 | 451 | @pytest.mark.asyncio 452 | async def test_set_monitoring_base(): 453 | server = await Cp.start_cp() 454 | async with websockets.connect( 455 | f'ws://0.0.0.0:9000/123', 456 | subprotocols=['ocpp2.0.1'] 457 | ) as ws: 458 | cp = central_system('123', ws) 459 | loop = asyncio.get_running_loop() 460 | loop.create_task(cp.start()) 461 | 462 | result = await cp.send_set_monitoring_base() 463 | assert result.status == 'Accepted' 464 | 465 | await cancel_tasks() 466 | server.close() 467 | 468 | 469 | @pytest.mark.asyncio 470 | async def test_set_monitoring_level(): 471 | server = await Cp.start_cp() 472 | async with websockets.connect( 473 | f'ws://0.0.0.0:9000/123', 474 | subprotocols=['ocpp2.0.1'] 475 | ) as ws: 476 | cp = central_system('123', ws) 477 | loop = asyncio.get_running_loop() 478 | loop.create_task(cp.start()) 479 | 480 | result = await cp.send_set_monitoring_level() 481 | assert result.status == 'Accepted' 482 | 483 | await cancel_tasks() 484 | server.close() 485 | 486 | 487 | @pytest.mark.asyncio 488 | async def test_set_network_profile(): 489 | server = await Cp.start_cp() 490 | async with websockets.connect( 491 | f'ws://0.0.0.0:9000/123', 492 | subprotocols=['ocpp2.0.1'] 493 | ) as ws: 494 | cp = central_system('123', ws) 495 | loop = asyncio.get_running_loop() 496 | loop.create_task(cp.start()) 497 | 498 | result = await cp.send_set_network_profile() 499 | assert result.status == 'Accepted' 500 | 501 | await cancel_tasks() 502 | server.close() 503 | 504 | 505 | @pytest.mark.asyncio 506 | async def test_set_variable_monitoring(): 507 | server = await Cp.start_cp() 508 | async with websockets.connect( 509 | f'ws://0.0.0.0:9000/123', 510 | subprotocols=['ocpp2.0.1'] 511 | ) as ws: 512 | cp = central_system('123', ws) 513 | loop = asyncio.get_running_loop() 514 | loop.create_task(cp.start()) 515 | 516 | result = await cp.send_set_variable_monitoring() 517 | assert result.set_monitoring_result[0]['status'] == 'Accepted' 518 | assert result.set_monitoring_result[0]['type'] == 'Delta' 519 | 520 | await cancel_tasks() 521 | server.close() 522 | 523 | 524 | @pytest.mark.asyncio 525 | async def test_set_variables(): 526 | server = await Cp.start_cp() 527 | async with websockets.connect( 528 | f'ws://0.0.0.0:9000/123', 529 | subprotocols=['ocpp2.0.1'] 530 | ) as ws: 531 | cp = central_system('123', ws) 532 | loop = asyncio.get_running_loop() 533 | loop.create_task(cp.start()) 534 | 535 | result = await cp.send_set_variables() 536 | assert result.set_variable_result[0]['attribute_status'] == 'Accepted' 537 | 538 | await cancel_tasks() 539 | server.close() 540 | 541 | 542 | @pytest.mark.asyncio 543 | async def test_trigger_message(): 544 | server = await Cp.start_cp() 545 | async with websockets.connect( 546 | f'ws://0.0.0.0:9000/123', 547 | subprotocols=['ocpp2.0.1'] 548 | ) as ws: 549 | cp = central_system('123', ws) 550 | loop = asyncio.get_running_loop() 551 | loop.create_task(cp.start()) 552 | 553 | result = await cp.send_trigger_message() 554 | assert result.status == 'Accepted' 555 | 556 | await cancel_tasks() 557 | server.close() 558 | 559 | 560 | @pytest.mark.asyncio 561 | async def test_unlock_connector(): 562 | server = await Cp.start_cp() 563 | async with websockets.connect( 564 | f'ws://0.0.0.0:9000/123', 565 | subprotocols=['ocpp2.0.1'] 566 | ) as ws: 567 | cp = central_system('123', ws) 568 | loop = asyncio.get_running_loop() 569 | loop.create_task(cp.start()) 570 | 571 | result = await cp.send_unlock_connector() 572 | assert result.status == 'Unlocked' 573 | 574 | await cancel_tasks() 575 | server.close() 576 | 577 | 578 | @pytest.mark.asyncio 579 | async def test_unpublish_firmware(): 580 | server = await Cp.start_cp() 581 | async with websockets.connect( 582 | f'ws://0.0.0.0:9000/123', 583 | subprotocols=['ocpp2.0.1'] 584 | ) as ws: 585 | cp = central_system('123', ws) 586 | loop = asyncio.get_running_loop() 587 | loop.create_task(cp.start()) 588 | 589 | result = await cp.send_unpublish_firmware() 590 | assert result.status == 'Unpublished' 591 | 592 | await cancel_tasks() 593 | server.close() 594 | 595 | 596 | # @pytest.mark.asyncio 597 | # async def test_update_firmware(): 598 | # server = await Cp.start_cp() 599 | # async with websockets.connect( 600 | # f'ws://0.0.0.0:9000/123', 601 | # subprotocols=['ocpp2.0.1'] 602 | # ) as ws: 603 | # cp = central_system('123', ws) 604 | # loop = asyncio.get_running_loop() 605 | # loop.create_task(cp.start()) 606 | # 607 | # result = await cp.send_update_firmware() 608 | # assert result.status == 'Accepted' 609 | # 610 | # await cancel_tasks() 611 | # server.close() 612 | -------------------------------------------------------------------------------- /tests/central_system.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import websockets 3 | from datetime import datetime 4 | import uuid 5 | from hashlib import sha256 6 | 7 | from faker import Faker 8 | 9 | from ocpp.routing import on 10 | from ocpp.v201 import ChargePoint as cp 11 | from ocpp.v201 import call_result, enums, call, datatypes 12 | 13 | 14 | logging.basicConfig(level=logging.INFO) 15 | 16 | 17 | fake = Faker() 18 | 19 | 20 | class ChargePoint(cp): 21 | @on(enums.Action.BootNotification) 22 | async def on_boot_notification(self, **kwargs): 23 | return call_result.BootNotificationPayload( 24 | current_time=datetime.utcnow().isoformat(), 25 | interval=10, 26 | status='Accepted' 27 | ) 28 | 29 | @on(enums.Action.StatusNotification) 30 | async def on_send_notification(self, **kwargs): 31 | return call_result.StatusNotificationPayload() 32 | 33 | @on(enums.Action.RequestStartTransaction) 34 | async def on_send_start_transaction(self, **kwargs): 35 | return call_result.RequestStartTransactionPayload( 36 | status=enums.RequestStartStopStatusType.accepted 37 | ) 38 | 39 | @on(enums.Action.Authorize) 40 | async def on_send_authorize(self, **kwargs): 41 | return call_result.AuthorizePayload( 42 | id_token_info={'status': enums.AuthorizationStatusType.accepted} 43 | ) 44 | 45 | @on(enums.Action.RequestStopTransaction) 46 | async def on_send_stop_transaction(self, **kwargs): 47 | return call_result.RequestStopTransactionPayload( 48 | status=enums.RequestStartStopStatusType.accepted 49 | ) 50 | 51 | @on(enums.Action.ClearCache) 52 | async def on_clear_cache(self, **kwargs): 53 | return call_result.ClearCachePayload( 54 | status=enums.ClearCacheStatusType.accepted 55 | ) 56 | 57 | @on(enums.Action.ClearedChargingLimit) 58 | async def on_cleared_charging_limit(self, **kwargs): 59 | return call_result.ClearedChargingLimitPayload() 60 | 61 | @on(enums.Action.FirmwareStatusNotification) 62 | async def on_firmware_status_notification(self, **kwargs): 63 | return call_result.FirmwareStatusNotificationPayload() 64 | 65 | @on(enums.Action.Get15118EVCertificate) 66 | async def on_get_15118ev_certificate(self, **kwargs): 67 | return call_result.Get15118EVCertificatePayload( 68 | status=enums.Iso15118EVCertificateStatusType.accepted, 69 | exi_response='Success message' 70 | ) 71 | 72 | @on(enums.Action.GetCertificateStatus) 73 | async def on_get_certificate_status(self, **kwargs): 74 | return call_result.GetCertificateStatusPayload( 75 | status=enums.GetCertificateStatusType.accepted 76 | ) 77 | 78 | @on(enums.Action.GetDisplayMessages) 79 | async def on_get_display_messages(self, **kwargs): 80 | return call_result.GetDisplayMessagesPayload( 81 | status=enums.GetDisplayMessagesStatusType.accepted 82 | ) 83 | 84 | @on(enums.Action.LogStatusNotification) 85 | async def on_log_status_notification(self, **kwargs): 86 | return call_result.LogStatusNotificationPayload() 87 | 88 | @on(enums.Action.MeterValues) 89 | async def on_meter_value(self, **kwargs): 90 | return call_result.MeterValuesPayload() 91 | 92 | @on(enums.Action.NotifyChargingLimit) 93 | async def on_notify_charging_limit(self, **kwargs): 94 | return call_result.NotifyChargingLimitPayload() 95 | 96 | @on(enums.Action.NotifyCustomerInformation) 97 | async def on_notify_customer_information(self, **kwargs): 98 | return call_result.NotifyCustomerInformationPayload() 99 | 100 | @on(enums.Action.NotifyDisplayMessages) 101 | async def on_notify_display_message(self, **kwargs): 102 | return call_result.NotifyDisplayMessagesPayload() 103 | 104 | @on(enums.Action.NotifyEVChargingNeeds) 105 | async def on_notify_ev_charging_needs(self, **kwargs): 106 | return call_result.NotifyEVChargingNeedsPayload( 107 | status=enums.NotifyEVChargingNeedsStatusType.accepted 108 | ) 109 | 110 | @on(enums.Action.NotifyEVChargingSchedule) 111 | async def on_notify_ev_charging_schedule(self, **kwargs): 112 | return call_result.NotifyEVChargingSchedulePayload( 113 | status=enums.GenericStatusType.accepted 114 | ) 115 | 116 | @on(enums.Action.NotifyEvent) 117 | async def on_notify_event(self, **kwargs): 118 | return call_result.NotifyEventPayload() 119 | 120 | @on(enums.Action.NotifyMonitoringReport) 121 | async def on_notify_monitoring_report(self, **kwargs): 122 | return call_result.NotifyMonitoringReportPayload() 123 | 124 | @on(enums.Action.NotifyReport) 125 | async def on_notify_report(self, **kwargs): 126 | return call_result.NotifyReportPayload() 127 | 128 | @on(enums.Action.PublishFirmwareStatusNotification) 129 | async def on_publish_firmware_status_notification(self, **kwargs): 130 | return call_result.PublishFirmwareStatusNotificationPayload() 131 | 132 | @on(enums.Action.ReportChargingProfiles) 133 | async def on_report_charging_profiles(self, **kwargs): 134 | return call_result.ReportChargingProfilesPayload() 135 | 136 | @on(enums.Action.Heartbeat) 137 | async def on_heartbeat(self, **kwargs): 138 | return call_result.HeartbeatPayload( 139 | current_time='10.10.2010' 140 | ) 141 | 142 | @on(enums.Action.ReservationStatusUpdate) 143 | async def on_reservation_status_update(self, **kwargs): 144 | return call_result.ReservationStatusUpdatePayload() 145 | 146 | @on(enums.Action.SecurityEventNotification) 147 | async def on_security_event_notification(self, **kwargs): 148 | return call_result.SecurityEventNotificationPayload() 149 | 150 | @on(enums.Action.SignCertificate) 151 | async def on_sign_certificate(self, **kwargs): 152 | return call_result.SignCertificatePayload( 153 | status=enums.GenericStatusType.accepted 154 | ) 155 | 156 | @on(enums.Action.TransactionEvent) 157 | async def on_transaction_event(self, **kwargs): 158 | return call_result.TransactionEventPayload( 159 | total_cost=100 160 | ) 161 | 162 | async def send_cancel_reservation(self): 163 | request = call.CancelReservationPayload( 164 | reservation_id=12 165 | ) 166 | response = await self.call(request) 167 | return response 168 | 169 | async def send_certificate_signed(self): 170 | request = call.CertificateSignedPayload( 171 | certificate_chain=fake.pystr(1, 12) 172 | ) 173 | response = await self.call(request) 174 | return response 175 | 176 | async def send_change_availability(self): 177 | request = call.ChangeAvailabilityPayload( 178 | operational_status=enums.OperationalStatusType.operative 179 | ) 180 | response = await self.call(request) 181 | return response 182 | 183 | async def send_clear_charging_profile(self): 184 | request = call.ClearChargingProfilePayload() 185 | response = await self.call(request) 186 | return response 187 | 188 | async def send_clear_display_message(self): 189 | request = call.ClearDisplayMessagePayload( 190 | id=fake.random_int(1, 100) 191 | ) 192 | response = await self.call(request) 193 | return response 194 | 195 | async def send_clear_variable_monitoring(self): 196 | request = call.ClearVariableMonitoringPayload( 197 | id=[fake.random_int(1, 100)] 198 | ) 199 | response = await self.call(request) 200 | return response 201 | 202 | async def send_cost_updated(self): 203 | request = call.CostUpdatedPayload( 204 | total_cost=fake.random_int(1, 100), 205 | transaction_id=str(uuid.uuid4()) 206 | ) 207 | response = await self.call(request) 208 | return response 209 | 210 | async def send_customer_info(self): 211 | request = call.CustomerInformationPayload( 212 | request_id=fake.random_int(1, 100), 213 | report=True, 214 | clear=True 215 | ) 216 | response = await self.call(request) 217 | return response 218 | 219 | async def send_data_transfer(self): 220 | request = call.DataTransferPayload( 221 | vendor_id=str(uuid.uuid4()) 222 | ) 223 | response = await self.call(request) 224 | return response 225 | 226 | async def send_delete_certificate(self): 227 | request = call.DeleteCertificatePayload( 228 | certificate_hash_data=datatypes.CertificateHashDataType( 229 | hash_algorithm=enums.HashAlgorithmType.sha256, 230 | issuer_name_hash=sha256(fake.pystr(1, 128).encode('utf-8')).hexdigest(), 231 | issuer_key_hash=sha256(fake.pystr(1, 128).encode('utf-8')).hexdigest(), 232 | serial_number=fake.pystr(1, 40) 233 | ) 234 | ) 235 | response = await self.call(request) 236 | return response 237 | 238 | async def send_get_base_report(self): 239 | request = call.GetBaseReportPayload( 240 | request_id=fake.random_int(1, 100), 241 | report_base=enums.ReportBaseType.configuration_inventory 242 | ) 243 | response = await self.call(request) 244 | return response 245 | 246 | async def send_get_charging_profiles(self): 247 | request = call.GetChargingProfilesPayload( 248 | request_id=fake.random_int(1, 100), 249 | charging_profile=datatypes.ChargingProfileCriterionType( 250 | stack_level=fake.random_int(min=0, max=100), 251 | charging_profile_purpose=enums.ChargingProfilePurposeType.tx_profile 252 | ) 253 | ) 254 | response = await self.call(request) 255 | return response 256 | 257 | async def send_get_composite_schedule(self): 258 | request = call.GetCompositeSchedulePayload( 259 | duration=fake.random_int(min=0, max=100), 260 | evse_id=fake.random_int(min=0, max=100) 261 | ) 262 | response = await self.call(request) 263 | return response 264 | 265 | async def send_get_display_messages(self): 266 | request = call.GetDisplayMessagesPayload( 267 | request_id=fake.random_int(min=0, max=100) 268 | ) 269 | response = await self.call(request) 270 | return response 271 | 272 | async def send_get_installed_certificate_ids(self): 273 | request = call.GetInstalledCertificateIdsPayload() 274 | response = await self.call(request) 275 | return response 276 | 277 | async def send_get_local_list_version(self): 278 | request = call.GetLocalListVersionPayload() 279 | response = await self.call(request) 280 | return response 281 | 282 | async def send_get_log(self): 283 | request = call.GetLogPayload( 284 | log_type=enums.LogType.security_log, 285 | request_id=fake.random_int(min=0, max=100), 286 | log=datatypes.LogParametersType( 287 | remote_location=fake.pystr(1, 128) 288 | ) 289 | ) 290 | response = await self.call(request) 291 | return response 292 | 293 | async def send_get_monitoring_report(self): 294 | request = call.GetMonitoringReportPayload( 295 | request_id=fake.random_int(min=0, max=100) 296 | ) 297 | response = await self.call(request) 298 | return response 299 | 300 | async def send_publish_firmware(self): 301 | request = call.PublishFirmwarePayload( 302 | location=fake.pystr(1, 512), 303 | checksum=fake.pystr(1, 20), 304 | request_id=fake.random_int(min=0, max=100) 305 | ) 306 | response = await self.call(request) 307 | return response 308 | 309 | async def send_reserve_now(self): 310 | request = call.ReserveNowPayload( 311 | id=fake.random_int(min=0, max=100), 312 | expiry_date_time=str(datetime.now()), 313 | id_token={ 314 | 'idToken': str(uuid.uuid4()), 315 | 'type': enums.IdTokenType.central 316 | } 317 | ) 318 | response = await self.call(request) 319 | return response 320 | 321 | async def send_reset(self): 322 | request = call.ResetPayload( 323 | type=enums.ResetType.immediate 324 | ) 325 | response = await self.call(request) 326 | return response 327 | 328 | async def send_send_local_list(self): 329 | request = call.SendLocalListPayload( 330 | version_number=fake.random_int(min=0, max=100), 331 | update_type=enums.UpdateType.full 332 | ) 333 | response = await self.call(request) 334 | return response 335 | 336 | async def send_set_charging_profile(self): 337 | request = call.SetChargingProfilePayload( 338 | evse_id=fake.random_int(min=0, max=100), 339 | charging_profile=datatypes.ChargingProfileType( 340 | id=fake.random_int(min=0, max=100), 341 | stack_level=fake.random_int(min=0, max=100), 342 | charging_profile_purpose=enums.ChargingProfilePurposeType.charging_station_max_profile, 343 | charging_profile_kind=enums.ChargingProfileKindType.absolute, 344 | charging_schedule=[datatypes.ChargingScheduleType( 345 | id=fake.random_int(min=0, max=100), 346 | charging_rate_unit=enums.ChargingRateUnitType.amps, 347 | charging_schedule_period=[datatypes.ChargingSchedulePeriodType( 348 | start_period=datetime.now().day, 349 | limit=fake.pyfloat(min_value=1, max_value=100) 350 | )] 351 | )] 352 | ) 353 | ) 354 | response = await self.call(request) 355 | return response 356 | 357 | async def send_set_display_message(self): 358 | request = call.SetDisplayMessagePayload( 359 | message=datatypes.MessageInfoType( 360 | id=fake.random_int(min=0, max=100), 361 | priority=enums.MessagePriorityType.normal_cycle, 362 | message=datatypes.MessageContentType( 363 | format=enums.MessageFormatType.ascii, 364 | content=fake.pystr(1, 512), 365 | language=fake.pystr(1, 8) 366 | ) 367 | ) 368 | ) 369 | response = await self.call(request) 370 | return response 371 | 372 | async def send_set_monitoring_base(self): 373 | request = call.SetMonitoringBasePayload( 374 | monitoring_base=enums.MonitorBaseType.all 375 | ) 376 | response = await self.call(request) 377 | return response 378 | 379 | async def send_set_monitoring_level(self): 380 | request = call.SetMonitoringLevelPayload( 381 | severity=fake.random_int(min=0, max=9) 382 | ) 383 | response = await self.call(request) 384 | return response 385 | 386 | async def send_set_network_profile(self): 387 | request = call.SetNetworkProfilePayload( 388 | configuration_slot=fake.random_int(min=0, max=100), 389 | connection_data=datatypes.NetworkConnectionProfileType( 390 | ocpp_version=enums.OCPPVersionType.ocpp20, 391 | ocpp_transport=enums.OCPPTransportType.json, 392 | ocpp_csms_url=fake.url(), 393 | message_timeout=fake.random_int(min=0, max=100), 394 | security_profile=fake.random_int(min=0, max=9), 395 | ocpp_interface=enums.OCPPInterfaceType.wired0 396 | ) 397 | ) 398 | response = await self.call(request) 399 | return response 400 | 401 | async def send_set_variable_monitoring(self): 402 | request = call.SetVariableMonitoringPayload( 403 | set_monitoring_data=[datatypes.SetMonitoringDataType( 404 | id=fake.random_int(min=0, max=100), 405 | value=fake.pyfloat(min_value=1, max_value=100), 406 | type=enums.MonitorType.periodic, 407 | severity=fake.random_int(min=0, max=9), 408 | component=datatypes.ComponentType( 409 | name=str(uuid.uuid4()), 410 | ), 411 | variable=datatypes.VariableType( 412 | name=str(uuid.uuid4()) 413 | ) 414 | )] 415 | ) 416 | response = await self.call(request) 417 | return response 418 | 419 | async def send_set_variables(self): 420 | request = call.SetVariablesPayload( 421 | set_variable_data=[datatypes.SetVariableDataType( 422 | attribute_type=enums.AttributeType.maxSet, 423 | attribute_value=fake.pystr(1, 1000), 424 | component=datatypes.ComponentType( 425 | name=str(uuid.uuid4()), 426 | ), 427 | variable=datatypes.VariableType( 428 | name=str(uuid.uuid4()) 429 | ) 430 | )] 431 | ) 432 | response = await self.call(request) 433 | return response 434 | 435 | async def send_trigger_message(self): 436 | request = call.TriggerMessagePayload( 437 | requested_message=enums.MessageTriggerType.log_status_notification 438 | ) 439 | response = await self.call(request) 440 | return response 441 | 442 | async def send_unlock_connector(self): 443 | request = call.UnlockConnectorPayload( 444 | evse_id=fake.random_int(min=0, max=100), 445 | connector_id=fake.random_int(min=0, max=100) 446 | ) 447 | response = await self.call(request) 448 | return response 449 | 450 | async def send_unpublish_firmware(self): 451 | request = call.UnpublishFirmwarePayload( 452 | checksum=fake.pystr(1, 32) 453 | ) 454 | response = await self.call(request) 455 | return response 456 | 457 | async def send_update_firmware(self): 458 | request = call.UpdateFirmwarePayload( 459 | request_id=fake.random_int(min=0, max=100), 460 | firmware=datatypes.FirmwareType( 461 | location=fake.pystr(1, 512), 462 | retrieval_date_time=str(datetime.now()) 463 | ) 464 | ) 465 | response = await self.call(request) 466 | return response 467 | 468 | 469 | async def on_connect(websocket, path): 470 | """ For every new charge point that connects, create a ChargePoint 471 | instance and start listening for messages. 472 | """ 473 | try: 474 | requested_protocols = websocket.request_headers[ 475 | 'Sec-WebSocket-Protocol'] 476 | except KeyError: 477 | logging.info("Client hasn't requested any Subprotocol. " 478 | "Closing Connection") 479 | if websocket.subprotocol: 480 | logging.info("Protocols Matched: %s", websocket.subprotocol) 481 | else: 482 | # In the websockets lib if no subprotocols are supported by the 483 | # client and the server, it proceeds without a subprotocol, 484 | # so we have to manually close the connection. 485 | logging.warning('Protocols Mismatched | Expected Subprotocols: %s,' 486 | ' but client supports %s | Closing connection', 487 | websocket.available_subprotocols, 488 | requested_protocols) 489 | return await websocket.close() 490 | 491 | charge_point_id = path.strip('/') 492 | cp = ChargePoint(charge_point_id, websocket) 493 | 494 | await cp.start() 495 | 496 | 497 | async def start_central_system(): 498 | server = await websockets.serve( 499 | on_connect, 500 | "0.0.0.0", 501 | 9000, 502 | subprotocols=['ocpp2.0.1'] 503 | ) 504 | logging.info("WebSocket Server Started") 505 | return server 506 | -------------------------------------------------------------------------------- /tests/test_send_messages_to_central_system.py: -------------------------------------------------------------------------------- 1 | import websockets 2 | import pytest 3 | from unittest.mock import patch 4 | import asyncio 5 | 6 | from faker import Faker 7 | 8 | from .central_system import start_central_system 9 | from ocpp_simulator.cp_management import cp as Cp 10 | 11 | 12 | fake = Faker() 13 | 14 | 15 | async def cancel_tasks(): 16 | for task in asyncio.all_tasks(): 17 | task.cancel() 18 | 19 | 20 | @patch("ocpp_simulator.cp_management.cp.ask_question") 21 | @patch("typer.prompt") 22 | @pytest.mark.asyncio 23 | async def test_status_and_boot_notifications(mock_typer, mock_ask_question): 24 | server = await start_central_system() 25 | async with websockets.connect( 26 | f'ws://0.0.0.0:9000/123', 27 | subprotocols=['ocpp2.0.1'] 28 | ) as ws: 29 | cp = Cp.ChargePoint('123', ws) 30 | loop = asyncio.get_running_loop() 31 | loop.create_task(cp.start()) 32 | 33 | mock_typer.return_value = '123' 34 | mock_ask_question.return_value = 'PowerUp' 35 | result = await cp.send_boot_notification() 36 | assert result.status == 'Accepted' 37 | assert result.interval == 10 38 | 39 | mock_typer.return_value = 1 40 | mock_ask_question.return_value = "Available" 41 | result = await cp.send_status_notification() 42 | assert str(result) == 'StatusNotificationPayload()' 43 | 44 | await cancel_tasks() 45 | server.close() 46 | 47 | 48 | @patch("ocpp_simulator.cp_management.cp.ask_question") 49 | @pytest.mark.asyncio 50 | async def test_authorize(mock_ask_question): 51 | server = await start_central_system() 52 | async with websockets.connect( 53 | f'ws://0.0.0.0:9000/123', 54 | subprotocols=['ocpp2.0.1'] 55 | ) as ws: 56 | cp = Cp.ChargePoint('123', ws) 57 | loop = asyncio.get_running_loop() 58 | loop.create_task(cp.start()) 59 | 60 | mock_ask_question.return_value = "MacAddress" 61 | result = await cp.send_authorize() 62 | assert result.id_token_info['status'] == 'Accepted' 63 | 64 | await cancel_tasks() 65 | server.close() 66 | 67 | 68 | @pytest.mark.asyncio 69 | async def test_clear_cache(): 70 | server = await start_central_system() 71 | async with websockets.connect( 72 | f'ws://0.0.0.0:9000/123', 73 | subprotocols=['ocpp2.0.1'] 74 | ) as ws: 75 | cp = Cp.ChargePoint('123', ws) 76 | loop = asyncio.get_running_loop() 77 | loop.create_task(cp.start()) 78 | 79 | result = await cp.send_clear_cache() 80 | assert result.status == 'Accepted' 81 | 82 | await cancel_tasks() 83 | server.close() 84 | 85 | 86 | @patch("ocpp_simulator.cp_management.cp.ask_question") 87 | @pytest.mark.asyncio 88 | async def test_cleared_charging_request(mock_ask_question): 89 | server = await start_central_system() 90 | async with websockets.connect( 91 | f'ws://0.0.0.0:9000/123', 92 | subprotocols=['ocpp2.0.1'] 93 | ) as ws: 94 | cp = Cp.ChargePoint('123', ws) 95 | loop = asyncio.get_running_loop() 96 | loop.create_task(cp.start()) 97 | 98 | mock_ask_question.return_value = "EMS" 99 | result = await cp.send_cleared_charging_request() 100 | assert str(result) == "ClearedChargingLimitPayload()" 101 | 102 | await cancel_tasks() 103 | server.close() 104 | 105 | 106 | @patch("ocpp_simulator.cp_management.cp.ask_question") 107 | @pytest.mark.asyncio 108 | async def test_firmware_status_notification(mock_ask_question): 109 | server = await start_central_system() 110 | async with websockets.connect( 111 | f'ws://0.0.0.0:9000/123', 112 | subprotocols=['ocpp2.0.1'] 113 | ) as ws: 114 | cp = Cp.ChargePoint('123', ws) 115 | loop = asyncio.get_running_loop() 116 | loop.create_task(cp.start()) 117 | 118 | mock_ask_question.return_value = "Downloaded" 119 | result = await cp.send_firmware_status_notification() 120 | assert str(result) == "FirmwareStatusNotificationPayload()" 121 | 122 | await cancel_tasks() 123 | server.close() 124 | 125 | 126 | @patch("ocpp_simulator.cp_management.cp.ask_question") 127 | @patch("typer.prompt") 128 | @pytest.mark.asyncio 129 | async def test_get_15118ev_certificate(mock_typer, mock_ask_question): 130 | server = await start_central_system() 131 | async with websockets.connect( 132 | f'ws://0.0.0.0:9000/123', 133 | subprotocols=['ocpp2.0.1'] 134 | ) as ws: 135 | cp = Cp.ChargePoint('123', ws) 136 | loop = asyncio.get_running_loop() 137 | loop.create_task(cp.start()) 138 | 139 | mock_typer.return_value = '1' 140 | mock_ask_question.return_value = "Install" 141 | result = await cp.send_get_15118ev_certificate() 142 | assert result.status == 'Accepted' 143 | assert result.exi_response == 'Success message' 144 | 145 | await cancel_tasks() 146 | server.close() 147 | 148 | 149 | # @patch("ocpp_simulator.cp_management.cp.ask_question") 150 | # @patch("typer.prompt") 151 | # @pytest.mark.asyncio 152 | # async def test_get_certificate_status(mock_typer, mock_ask_question): 153 | # server = await start_central_system() 154 | # async with websockets.connect( 155 | # f'ws://0.0.0.0:9000/123', 156 | # subprotocols=['ocpp2.0.1'] 157 | # ) as ws: 158 | # cp = Cp.ChargePoint('123', ws) 159 | # loop = asyncio.get_running_loop() 160 | # loop.create_task(cp.start()) 161 | # 162 | # mock_typer.side_effect = [fake.pystr(1, 128), fake.pystr(1, 128), fake.pystr(1, 40), fake.url()] 163 | # mock_ask_question.return_value = "Central" 164 | # result = await cp.send_get_certificate_status() 165 | # assert result.status == 'Accepted' 166 | # 167 | # await cancel_tasks() 168 | # server.close() 169 | 170 | 171 | @patch("typer.prompt") 172 | @pytest.mark.asyncio 173 | async def test_get_display_messages(mock_typer): 174 | server = await start_central_system() 175 | async with websockets.connect( 176 | f'ws://0.0.0.0:9000/123', 177 | subprotocols=['ocpp2.0.1'] 178 | ) as ws: 179 | cp = Cp.ChargePoint('123', ws) 180 | loop = asyncio.get_running_loop() 181 | loop.create_task(cp.start()) 182 | 183 | mock_typer.return_value = 1 184 | result = await cp.send_get_display_messages() 185 | assert result.status == 'Accepted' 186 | 187 | await cancel_tasks() 188 | server.close() 189 | 190 | 191 | @patch("ocpp_simulator.cp_management.cp.ask_question") 192 | @pytest.mark.asyncio 193 | async def test_log_status_notification(mock_ask_question): 194 | server = await start_central_system() 195 | async with websockets.connect( 196 | f'ws://0.0.0.0:9000/123', 197 | subprotocols=['ocpp2.0.1'] 198 | ) as ws: 199 | cp = Cp.ChargePoint('123', ws) 200 | loop = asyncio.get_running_loop() 201 | loop.create_task(cp.start()) 202 | 203 | mock_ask_question.return_value = "Uploaded" 204 | result = await cp.send_log_status_notification() 205 | assert str(result) == "LogStatusNotificationPayload()" 206 | 207 | await cancel_tasks() 208 | server.close() 209 | 210 | 211 | @patch("ocpp_simulator.cp_management.cp.ask_question") 212 | @patch("typer.prompt") 213 | @pytest.mark.asyncio 214 | async def test_meter_value(mock_typer, mock_ask_question): 215 | server = await start_central_system() 216 | async with websockets.connect( 217 | f'ws://0.0.0.0:9000/123', 218 | subprotocols=['ocpp2.0.1'] 219 | ) as ws: 220 | cp = Cp.ChargePoint('123', ws) 221 | loop = asyncio.get_running_loop() 222 | loop.create_task(cp.start()) 223 | 224 | mock_typer.side_effect = [fake.random_int(), fake.random_int()] 225 | mock_ask_question.side_effect = ["Bytes", "Transaction.End", "Current.Export", "L3"] 226 | await cp.send_meter_value() 227 | 228 | await cancel_tasks() 229 | server.close() 230 | 231 | 232 | @patch("ocpp_simulator.cp_management.cp.ask_question") 233 | @pytest.mark.asyncio 234 | async def test_notify_charging_limit(mock_ask_question): 235 | server = await start_central_system() 236 | async with websockets.connect( 237 | f'ws://0.0.0.0:9000/123', 238 | subprotocols=['ocpp2.0.1'] 239 | ) as ws: 240 | cp = Cp.ChargePoint('123', ws) 241 | loop = asyncio.get_running_loop() 242 | loop.create_task(cp.start()) 243 | 244 | mock_ask_question.return_value = "EMS" 245 | result = await cp.send_notify_charging_limit() 246 | assert str(result) == "NotifyChargingLimitPayload()" 247 | 248 | await cancel_tasks() 249 | server.close() 250 | 251 | 252 | @patch("typer.prompt") 253 | @pytest.mark.asyncio 254 | async def test_notify_customer_information(mock_typer): 255 | server = await start_central_system() 256 | async with websockets.connect( 257 | f'ws://0.0.0.0:9000/123', 258 | subprotocols=['ocpp2.0.1'] 259 | ) as ws: 260 | cp = Cp.ChargePoint('123', ws) 261 | loop = asyncio.get_running_loop() 262 | loop.create_task(cp.start()) 263 | 264 | mock_typer.side_effect = [fake.pystr(1, 12), fake.random_int(min=0, max=10), fake.random_int(min=0, max=10)] 265 | result = await cp.send_notify_customer_information() 266 | assert str(result) == "NotifyCustomerInformationPayload()" 267 | 268 | await cancel_tasks() 269 | server.close() 270 | 271 | 272 | @patch("typer.prompt") 273 | @pytest.mark.asyncio 274 | async def test_notify_display_message(mock_typer): 275 | server = await start_central_system() 276 | async with websockets.connect( 277 | f'ws://0.0.0.0:9000/123', 278 | subprotocols=['ocpp2.0.1'] 279 | ) as ws: 280 | cp = Cp.ChargePoint('123', ws) 281 | loop = asyncio.get_running_loop() 282 | loop.create_task(cp.start()) 283 | 284 | mock_typer.return_value = fake.random_int(min=0, max=10) 285 | result = await cp.send_notify_display_messages() 286 | assert str(result) == "NotifyDisplayMessagesPayload()" 287 | 288 | await cancel_tasks() 289 | server.close() 290 | 291 | 292 | # @patch("ocpp_simulator.cp_management.cp.ask_question") 293 | # @patch("typer.prompt") 294 | # @pytest.mark.asyncio 295 | # async def test_notify_ev_charging_needs(mock_typer, mock_ask_question): 296 | # server = await start_central_system() 297 | # async with websockets.connect( 298 | # f'ws://0.0.0.0:9000/123', 299 | # subprotocols=['ocpp2.0.1'] 300 | # ) as ws: 301 | # cp = Cp.ChargePoint('123', ws) 302 | # loop = asyncio.get_running_loop() 303 | # loop.create_task(cp.start()) 304 | # 305 | # mock_typer.return_value = fake.random_int(min=0, max=100) 306 | # mock_ask_question.return_value = "AC_single_phase" 307 | # result = await cp.send_notify_ev_charging_needs() 308 | # assert result.status == 'Accepted' 309 | # 310 | # await cancel_tasks() 311 | # server.close() 312 | 313 | 314 | @patch("ocpp_simulator.cp_management.cp.ask_question") 315 | @patch("typer.prompt") 316 | @pytest.mark.asyncio 317 | async def test_notify_ev_charging_schedule(mock_typer, mock_ask_question): 318 | server = await start_central_system() 319 | async with websockets.connect( 320 | f'ws://0.0.0.0:9000/123', 321 | subprotocols=['ocpp2.0.1'] 322 | ) as ws: 323 | cp = Cp.ChargePoint('123', ws) 324 | loop = asyncio.get_running_loop() 325 | loop.create_task(cp.start()) 326 | 327 | mock_typer.return_value = fake.random_int(min=0, max=100) 328 | mock_ask_question.return_value = "W" 329 | result = await cp.send_notify_ev_charging_schedule() 330 | assert result.status == 'Accepted' 331 | 332 | await cancel_tasks() 333 | server.close() 334 | 335 | 336 | @patch("ocpp_simulator.cp_management.cp.ask_question") 337 | @patch("typer.prompt") 338 | @pytest.mark.asyncio 339 | async def test_notify_event(mock_typer, mock_ask_question): 340 | server = await start_central_system() 341 | async with websockets.connect( 342 | f'ws://0.0.0.0:9000/123', 343 | subprotocols=['ocpp2.0.1'] 344 | ) as ws: 345 | cp = Cp.ChargePoint('123', ws) 346 | loop = asyncio.get_running_loop() 347 | loop.create_task(cp.start()) 348 | 349 | mock_typer.side_effect = [ 350 | fake.random_int(min=0, max=100), fake.random_int(min=0, max=100), fake.pystr(1, 50), 351 | fake.pystr(1, 50), fake.pystr(1, 50), fake.pystr(1, 50) 352 | ] 353 | mock_ask_question.side_effect = ["Alerting", "HardWiredNotification"] 354 | result = await cp.send_notify_event() 355 | assert str(result) == 'NotifyEventPayload()' 356 | 357 | await cancel_tasks() 358 | server.close() 359 | 360 | 361 | @patch("typer.prompt") 362 | @pytest.mark.asyncio 363 | async def test_notify_monitoring_report(mock_typer): 364 | server = await start_central_system() 365 | async with websockets.connect( 366 | f'ws://0.0.0.0:9000/123', 367 | subprotocols=['ocpp2.0.1'] 368 | ) as ws: 369 | cp = Cp.ChargePoint('123', ws) 370 | loop = asyncio.get_running_loop() 371 | loop.create_task(cp.start()) 372 | 373 | mock_typer.side_effect = [fake.random_int(min=0, max=100), fake.random_int(min=0, max=100)] 374 | result = await cp.send_notify_monitoring_report() 375 | assert str(result) == 'NotifyMonitoringReportPayload()' 376 | 377 | await cancel_tasks() 378 | server.close() 379 | 380 | 381 | @patch("typer.prompt") 382 | @pytest.mark.asyncio 383 | async def test_notify_report(mock_typer): 384 | server = await start_central_system() 385 | async with websockets.connect( 386 | f'ws://0.0.0.0:9000/123', 387 | subprotocols=['ocpp2.0.1'] 388 | ) as ws: 389 | cp = Cp.ChargePoint('123', ws) 390 | loop = asyncio.get_running_loop() 391 | loop.create_task(cp.start()) 392 | 393 | mock_typer.side_effect = [fake.random_int(min=0, max=100), fake.random_int(min=0, max=100)] 394 | result = await cp.send_notify_report() 395 | assert str(result) == 'NotifyReportPayload()' 396 | 397 | await cancel_tasks() 398 | server.close() 399 | 400 | 401 | @patch("ocpp_simulator.cp_management.cp.ask_question") 402 | @pytest.mark.asyncio 403 | async def test_publish_firmware_status_notification(mock_ask_question): 404 | server = await start_central_system() 405 | async with websockets.connect( 406 | f'ws://0.0.0.0:9000/123', 407 | subprotocols=['ocpp2.0.1'] 408 | ) as ws: 409 | cp = Cp.ChargePoint('123', ws) 410 | loop = asyncio.get_running_loop() 411 | loop.create_task(cp.start()) 412 | 413 | mock_ask_question.return_value = "Downloaded" 414 | result = await cp.send_publish_firmware_status_notification() 415 | assert str(result) == 'PublishFirmwareStatusNotificationPayload()' 416 | 417 | await cancel_tasks() 418 | server.close() 419 | 420 | 421 | @patch("ocpp_simulator.cp_management.cp.ask_question") 422 | @patch("typer.prompt") 423 | @pytest.mark.asyncio 424 | async def test_report_charging_profiles(mock_typer, mock_ask_question): 425 | server = await start_central_system() 426 | async with websockets.connect( 427 | f'ws://0.0.0.0:9000/123', 428 | subprotocols=['ocpp2.0.1'] 429 | ) as ws: 430 | cp = Cp.ChargePoint('123', ws) 431 | loop = asyncio.get_running_loop() 432 | loop.create_task(cp.start()) 433 | 434 | mock_typer.side_effect = [ 435 | fake.random_int(min=0, max=100), fake.random_int(min=0, max=100), fake.random_int(min=0, max=100), 436 | fake.random_int(min=0, max=100), fake.random_int(min=0, max=100), fake.pyfloat(min_value=1, max_value=100) 437 | ] 438 | mock_ask_question.side_effect = ["EMS", "ChargingStationExternalConstraints", "Relative", "W"] 439 | result = await cp.send_report_charging_profiles() 440 | assert str(result) == "ReportChargingProfilesPayload()" 441 | 442 | await cancel_tasks() 443 | server.close() 444 | 445 | 446 | @pytest.mark.asyncio 447 | async def test_heartbeat(): 448 | server = await start_central_system() 449 | async with websockets.connect( 450 | f'ws://0.0.0.0:9000/123', 451 | subprotocols=['ocpp2.0.1'] 452 | ) as ws: 453 | cp = Cp.ChargePoint('123', ws) 454 | loop = asyncio.get_running_loop() 455 | loop.create_task(cp.start()) 456 | 457 | result = await cp.send_heartbeat() 458 | assert result.current_time == '10.10.2010' 459 | 460 | await cancel_tasks() 461 | server.close() 462 | 463 | 464 | @patch("ocpp_simulator.cp_management.cp.ask_question") 465 | @patch("typer.prompt") 466 | @pytest.mark.asyncio 467 | async def test_start_and_stop_transaction(mock_typer, mock_ask_question): 468 | server = await start_central_system() 469 | async with websockets.connect( 470 | f'ws://0.0.0.0:9000/123', 471 | subprotocols=['ocpp2.0.1'] 472 | ) as ws: 473 | cp = Cp.ChargePoint('123', ws) 474 | loop = asyncio.get_running_loop() 475 | loop.create_task(cp.start()) 476 | 477 | mock_typer.return_value = 1 478 | mock_ask_question.return_value = "Central" 479 | result = await cp.send_start_transaction() 480 | assert result.status == 'Accepted' 481 | result = await cp.send_start_transaction() 482 | assert result.status == 'Accepted' 483 | 484 | await cancel_tasks() 485 | server.close() 486 | 487 | 488 | @patch("ocpp_simulator.cp_management.cp.ask_question") 489 | @patch("typer.prompt") 490 | @pytest.mark.asyncio 491 | async def test_reservation_status_update(mock_typer, mock_ask_question): 492 | server = await start_central_system() 493 | async with websockets.connect( 494 | f'ws://0.0.0.0:9000/123', 495 | subprotocols=['ocpp2.0.1'] 496 | ) as ws: 497 | cp = Cp.ChargePoint('123', ws) 498 | loop = asyncio.get_running_loop() 499 | loop.create_task(cp.start()) 500 | 501 | mock_typer.return_value = fake.pyint(1, 100) 502 | mock_ask_question.return_value = "Removed" 503 | result = await cp.send_reservation_status_update() 504 | assert str(result) == "ReservationStatusUpdatePayload()" 505 | 506 | await cancel_tasks() 507 | server.close() 508 | 509 | 510 | @patch("ocpp_simulator.cp_management.cp.ask_question") 511 | @pytest.mark.asyncio 512 | async def test_security_event_notification(mock_ask_question): 513 | server = await start_central_system() 514 | async with websockets.connect( 515 | f'ws://0.0.0.0:9000/123', 516 | subprotocols=['ocpp2.0.1'] 517 | ) as ws: 518 | cp = Cp.ChargePoint('123', ws) 519 | loop = asyncio.get_running_loop() 520 | loop.create_task(cp.start()) 521 | 522 | mock_ask_question.return_value = "FirmwareUpdated" 523 | result = await cp.send_security_event_notification() 524 | assert str(result) == "SecurityEventNotificationPayload()" 525 | 526 | await cancel_tasks() 527 | server.close() 528 | 529 | 530 | @patch("typer.prompt") 531 | @pytest.mark.asyncio 532 | async def test_sign_certificate(mock_typer): 533 | server = await start_central_system() 534 | async with websockets.connect( 535 | f'ws://0.0.0.0:9000/123', 536 | subprotocols=['ocpp2.0.1'] 537 | ) as ws: 538 | cp = Cp.ChargePoint('123', ws) 539 | loop = asyncio.get_running_loop() 540 | loop.create_task(cp.start()) 541 | 542 | mock_typer.return_value = fake.pystr(1, 100) 543 | result = await cp.send_sign_certificate() 544 | assert result.status == 'Accepted' 545 | 546 | await cancel_tasks() 547 | server.close() 548 | 549 | 550 | @patch("ocpp_simulator.cp_management.cp.ask_question") 551 | @patch("typer.prompt") 552 | @pytest.mark.asyncio 553 | async def test_transaction_event(mock_typer, mock_ask_question): 554 | server = await start_central_system() 555 | async with websockets.connect( 556 | f'ws://0.0.0.0:9000/123', 557 | subprotocols=['ocpp2.0.1'] 558 | ) as ws: 559 | cp = Cp.ChargePoint('123', ws) 560 | loop = asyncio.get_running_loop() 561 | loop.create_task(cp.start()) 562 | 563 | mock_typer.side_effect = [fake.pystr(1, 36), fake.pyint(1, 100)] 564 | mock_ask_question.side_effect = ["Started", "Authorized"] 565 | result = await cp.send_transaction_event() 566 | assert result.total_cost == 100 567 | 568 | await cancel_tasks() 569 | server.close() 570 | -------------------------------------------------------------------------------- /ocpp_simulator/cp_management/cp.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from hashlib import sha256 3 | from datetime import datetime, timedelta 4 | import logging 5 | 6 | import typer 7 | import questionary 8 | import websockets 9 | 10 | from faker import Faker 11 | from ocpp.routing import on 12 | from ocpp.v201 import ChargePoint as Cp, call, call_result, datatypes, enums 13 | 14 | logging.basicConfig(level=logging.INFO) 15 | 16 | fake = Faker() 17 | 18 | 19 | async def ask_question(enum_type, question: str): 20 | choices = [limit.value for limit in enum_type] 21 | answer = await questionary.select( 22 | question, 23 | choices=list(choices) 24 | ).ask_async() 25 | return answer 26 | 27 | 28 | # pylint: disable=too-many-public-methods 29 | class ChargePoint(Cp): 30 | 31 | async def send_boot_notification(self): 32 | model = typer.prompt('Enter charge point model', default=fake.pystr(1, 12)) 33 | vendor_name = typer.prompt('Enter charge point vendor name', default=fake.pystr(1, 12)) 34 | reason_type = await ask_question(enums.BootReasonType, "Which reason type:") 35 | request = call.BootNotificationPayload( 36 | charging_station={ 37 | 'model': model, 38 | 'vendor_name': vendor_name 39 | }, 40 | reason=reason_type 41 | ) 42 | response = await self.call(request) 43 | return response 44 | 45 | async def send_status_notification(self): 46 | connector_id = int(typer.prompt('Enter connector ID', default=fake.random_int())) 47 | evse_id = int(typer.prompt('Enter EVSE ID', default=fake.random_int())) 48 | connector_status = await ask_question(enums.ConnectorStatusType, "Which connector status type:") 49 | request = call.StatusNotificationPayload( 50 | connector_id=connector_id, 51 | connector_status=connector_status, 52 | timestamp=str(datetime.now()), 53 | evse_id=evse_id 54 | ) 55 | response = await self.call(request) 56 | return response 57 | 58 | async def send_authorize(self): 59 | id_type = await ask_question(enums.IdTokenType, "Which ID token type:") 60 | request = call.AuthorizePayload( 61 | id_token={ 62 | 'idToken': str(uuid.uuid4()), 63 | 'type': id_type 64 | }, 65 | ) 66 | response = await self.call(request) 67 | return response 68 | 69 | async def send_clear_cache(self): 70 | request = call.ClearCachePayload() 71 | response = await self.call(request) 72 | return response 73 | 74 | async def send_cleared_charging_request(self): 75 | charging_limit_source = await ask_question(enums.ChargingLimitSourceType, "Which charging limit source:") 76 | request = call.ClearedChargingLimitPayload( 77 | charging_limit_source=charging_limit_source 78 | ) 79 | response = await self.call(request) 80 | return response 81 | 82 | async def send_firmware_status_notification(self): 83 | firmware_status = await ask_question(enums.FirmwareStatusType, "Which firmware status notification:") 84 | request = call.FirmwareStatusNotificationPayload( 85 | status=firmware_status 86 | ) 87 | response = await self.call(request) 88 | return response 89 | 90 | async def send_get_15118ev_certificate(self): 91 | iso15118_schema_version = typer.prompt('Enter ISO15188 version', default=fake.pystr(1, 12)) 92 | certificate_action_type = await ask_question(enums.CertificateActionType, "Which certificate action type:") 93 | request = call.Get15118EVCertificatePayload( 94 | iso15118_schema_version=iso15118_schema_version, 95 | action=certificate_action_type, 96 | exi_request=str(uuid.uuid4()) 97 | ) 98 | response = await self.call(request) 99 | return response 100 | 101 | async def send_get_certificate_status(self): 102 | issuer_name = typer.prompt('Enter issuer name', default=fake.pystr(1, 128)) 103 | issuer_key = typer.prompt('Enter issuer key', default=fake.pystr(1, 128)) 104 | serial_number = typer.prompt('Enter serial number', default=fake.pystr(1, 40)) 105 | responder_url = typer.prompt('Enter responder URL', default=fake.url()) 106 | request = call.GetCertificateStatusPayload( 107 | ocsp_request_data=datatypes.OCSPRequestDataType( 108 | hash_algorithm=enums.HashAlgorithmType.sha256, 109 | issuer_name_hash=sha256(issuer_name.encode('utf-8')).hexdigest(), 110 | issuer_key_hash=sha256(issuer_key.encode('utf-8')).hexdigest(), 111 | responder_url=responder_url, 112 | serial_number=serial_number 113 | ) 114 | ) 115 | response = await self.call(request) 116 | return response 117 | 118 | async def send_get_display_messages(self): 119 | request_id = typer.prompt('Enter serial number', fake.random_int(min=0, max=10)) 120 | request = call.GetDisplayMessagesPayload( 121 | request_id=request_id 122 | ) 123 | response = await self.call(request) 124 | return response 125 | 126 | async def send_log_status_notification(self): 127 | log_status = await ask_question(enums.UploadLogStatusType, "Which log status notification:") 128 | request = call.LogStatusNotificationPayload( 129 | status=log_status 130 | ) 131 | response = await self.call(request) 132 | return response 133 | 134 | async def send_meter_value(self): 135 | unit = await ask_question(enums.UnitOfMeasureType, "Which measure unit type:") 136 | evse_id = int(typer.prompt('Enter EVSE ID', default=fake.random_int())) 137 | value = int(typer.prompt('Enter sample value', default=fake.random_int())) 138 | value_context = await ask_question(enums.ReadingContextType, "Which value context type:") 139 | value_measurand = await ask_question(enums.MeasurandType, "Which value measurand type:") 140 | value_phase = await ask_question(enums.PhaseType, "Which value phase type:") 141 | request = call.MeterValuesPayload( 142 | evse_id=evse_id, 143 | meter_value=[ 144 | datatypes.MeterValueType( 145 | timestamp=str(datetime.now()), 146 | sampled_value=[ 147 | datatypes.SampledValueType( 148 | value=value, 149 | context=value_context, 150 | measurand=value_measurand, 151 | phase=None, 152 | location=None, 153 | unit_of_measure=datatypes.UnitOfMeasureType( 154 | unit=unit 155 | ) 156 | ), 157 | datatypes.SampledValueType( 158 | value=value, 159 | context=value_context, 160 | measurand=value_measurand, 161 | phase=value_phase, 162 | location=None, 163 | unit_of_measure=datatypes.UnitOfMeasureType( 164 | unit=unit 165 | ) 166 | ), 167 | ] 168 | ), 169 | datatypes.MeterValueType( 170 | timestamp=str(datetime.now() + timedelta(seconds=5)), 171 | sampled_value=[ 172 | datatypes.SampledValueType( 173 | value=value, 174 | context=value_context, 175 | measurand=value_measurand, 176 | phase=None, 177 | location=None, 178 | unit_of_measure=datatypes.UnitOfMeasureType( 179 | unit=unit 180 | ) 181 | ), 182 | datatypes.SampledValueType( 183 | value=value, 184 | context=value_context, 185 | measurand=value_measurand, 186 | phase=value_phase, 187 | location=None, 188 | unit_of_measure=datatypes.UnitOfMeasureType( 189 | unit=unit 190 | ) 191 | ), 192 | ] 193 | ), 194 | ] 195 | ) 196 | response = await self.call(request) 197 | return response 198 | 199 | async def send_notify_charging_limit(self): 200 | charging_limit_source = await ask_question(enums.ChargingLimitSourceType, "Which charging limit source:") 201 | request = call.NotifyChargingLimitPayload( 202 | charging_limit={ 203 | 'charging_limit_source': charging_limit_source, 204 | 'is_grid_critical': True 205 | } 206 | ) 207 | response = await self.call(request) 208 | return response 209 | 210 | async def send_notify_customer_information(self): 211 | data = typer.prompt('Enter customer information data', fake.pystr(1, 12)) 212 | seq_no = typer.prompt('Enter sequence number', fake.random_int(min=0, max=10)) 213 | request_id = typer.prompt('Enter request ID', fake.random_int(min=0, max=10)) 214 | request = call.NotifyCustomerInformationPayload( 215 | data=data, 216 | seq_no=seq_no, 217 | generated_at=str(datetime.now()), 218 | request_id=request_id 219 | ) 220 | response = await self.call(request) 221 | return response 222 | 223 | async def send_notify_display_messages(self): 224 | request_id = typer.prompt('Enter request ID', fake.random_int(min=0, max=10)) 225 | request = call.NotifyDisplayMessagesPayload( 226 | request_id=request_id 227 | ) 228 | response = await self.call(request) 229 | return response 230 | 231 | async def send_notify_ev_charging_needs(self): 232 | evse_id = typer.prompt('Enter EVSE ID', fake.random_int(min=0, max=100)) 233 | energy_amount = typer.prompt('Enter energy amount', fake.random_int(min=0, max=100)) 234 | ev_min_current = typer.prompt('Enter EV minimum current', fake.random_int(min=0, max=100)) 235 | ev_max_current = typer.prompt('Enter EV maximum current', fake.random_int(min=ev_min_current)) 236 | ev_max_voltage = typer.prompt('Enter EV maximum voltage', fake.random_int(min=0, max=100)) 237 | request_energy_transfer = await ask_question(enums.EnergyTransferModeType, "Which energy transfer:") 238 | request = call.NotifyEVChargingNeedsPayload( 239 | evse_id=evse_id, 240 | charging_needs=datatypes.ChargingNeedsType( 241 | request_energy_transfer=request_energy_transfer, 242 | ac_charging_parameters=datatypes.ACChargingParametersType( 243 | energy_amount=energy_amount, 244 | ev_min_current=ev_min_current, 245 | ev_max_current=ev_max_current, 246 | ev_max_voltage=ev_max_voltage 247 | ) 248 | ) 249 | ) 250 | response = await self.call(request) 251 | return response 252 | 253 | async def send_notify_ev_charging_schedule(self): 254 | evse_id = typer.prompt('Enter EVSE ID', fake.random_int(min=0, max=100)) 255 | charging_schedule_period_limit = typer.prompt('Enter charging schedule period limit', 256 | fake.pyfloat(min_value=1, max_value=100, positive=True)) 257 | charging_rate_unit = await ask_question(enums.ChargingRateUnitType, "Which charging rate unit:") 258 | request = call.NotifyEVChargingSchedulePayload( 259 | time_base=str(datetime.now()), 260 | evse_id=evse_id, 261 | charging_schedule=datatypes.ChargingScheduleType( 262 | id=fake.random_int(), 263 | charging_rate_unit=charging_rate_unit, 264 | charging_schedule_period=[datatypes.ChargingSchedulePeriodType( 265 | start_period=datetime.now().day, 266 | limit=charging_schedule_period_limit 267 | )] 268 | ) 269 | ) 270 | response = await self.call(request) 271 | return response 272 | 273 | async def send_notify_event(self): 274 | seq_no = typer.prompt('Enter sequence number', fake.random_int(min=0, max=100)) 275 | event_id = typer.prompt('Enter event ID', fake.random_int(min=0, max=100)) 276 | actual_value = typer.prompt('Enter event data actual value', fake.pystr(1, 50)) 277 | component_name = typer.prompt('Enter event data component name', fake.pystr(1, 50)) 278 | component_instance = typer.prompt('Enter event data component instance', fake.pystr(1, 50)) 279 | variable_name = typer.prompt('Enter event data variable name', fake.pystr(1, 50)) 280 | trigger = await ask_question(enums.EventTriggerType, "Which trigger type:") 281 | event_notification_type = await ask_question(enums.EventNotificationType, "Which event notification type:") 282 | request = call.NotifyEventPayload( 283 | generated_at=str(datetime.now()), 284 | seq_no=seq_no, 285 | event_data=[datatypes.EventDataType( 286 | event_id=event_id, 287 | timestamp=str(datetime.now()), 288 | trigger=trigger, 289 | actual_value=actual_value, 290 | event_notification_type=event_notification_type, 291 | component=datatypes.ComponentType( 292 | name=component_name, 293 | instance=component_instance 294 | ), 295 | variable=datatypes.VariableType( 296 | name=variable_name 297 | ) 298 | )] 299 | ) 300 | response = await self.call(request) 301 | return response 302 | 303 | async def send_notify_monitoring_report(self): 304 | request_id = typer.prompt('Enter request ID', fake.random_int(min=0, max=100)) 305 | seq_no = typer.prompt('Enter sequence number', fake.random_int(min=0, max=100)) 306 | request = call.NotifyMonitoringReportPayload( 307 | request_id=request_id, 308 | seq_no=seq_no, 309 | generated_at=str(datetime.now()) 310 | ) 311 | response = await self.call(request) 312 | return response 313 | 314 | async def send_notify_report(self): 315 | request_id = typer.prompt('Enter request ID', fake.random_int(min=0, max=100)) 316 | seq_no = typer.prompt('Enter sequence number', fake.random_int(min=0, max=100)) 317 | request = call.NotifyReportPayload( 318 | request_id=request_id, 319 | generated_at=str(datetime.now()), 320 | seq_no=seq_no 321 | ) 322 | response = await self.call(request) 323 | return response 324 | 325 | async def send_publish_firmware_status_notification(self): 326 | status = await ask_question(enums.PublishFirmwareStatusType, "Which publish firmware status:") 327 | request = call.PublishFirmwareStatusNotificationPayload( 328 | status=status 329 | ) 330 | response = await self.call(request) 331 | return response 332 | 333 | async def send_report_charging_profiles(self): 334 | evse_id = typer.prompt('Enter EVSE ID', fake.random_int(min=0, max=100)) 335 | request_id = typer.prompt('Enter request ID', fake.random_int(min=0, max=100)) 336 | charging_profile_id = typer.prompt('Enter charging profile ID', fake.random_int(min=0, max=100)) 337 | stack_level = typer.prompt('Enter stack level', fake.random_int(min=0, max=100)) 338 | charging_schedule_id = typer.prompt('Enter charging schedule ID', fake.random_int(min=0, max=100)) 339 | charging_schedule_period_limit = typer.prompt('Enter charging schedule period limit', 340 | fake.pyfloat(min_value=1, max_value=100, positive=True)) 341 | charging_limit_type = await ask_question(enums.ChargingLimitSourceType, "Which charging limit type:") 342 | charging_profile_purpose = await ask_question(enums.ChargingProfilePurposeType, 343 | "Which charging profile purpose:") 344 | charging_profile_kind = await ask_question(enums.ChargingProfileKindType, "Which charging profile kind:") 345 | charging_rate_unit = await ask_question(enums.ChargingRateUnitType, "Which charging rate unit:") 346 | request = call.ReportChargingProfilesPayload( 347 | request_id=request_id, 348 | charging_limit_source=charging_limit_type, 349 | evse_id=evse_id, 350 | charging_profile=[datatypes.ChargingProfileType( 351 | id=charging_profile_id, 352 | stack_level=stack_level, 353 | charging_profile_purpose=charging_profile_purpose, 354 | charging_profile_kind=charging_profile_kind, 355 | charging_schedule=[datatypes.ChargingScheduleType( 356 | id=charging_schedule_id, 357 | charging_rate_unit=charging_rate_unit, 358 | charging_schedule_period=[datatypes.ChargingSchedulePeriodType( 359 | start_period=datetime.now().day, 360 | limit=charging_schedule_period_limit 361 | )] 362 | )] 363 | )] 364 | ) 365 | response = await self.call(request) 366 | return response 367 | 368 | async def send_heartbeat(self): 369 | request = call.HeartbeatPayload() 370 | response = await self.call(request) 371 | return response 372 | 373 | async def send_start_transaction(self): 374 | remote_start_id = typer.prompt('Enter remote start', fake.random_int(min=0, max=100)) 375 | id_token_type = await ask_question(enums.IdTokenType, "Which ID token type:") 376 | request = call.RequestStartTransactionPayload( 377 | id_token={ 378 | 'idToken': str(uuid.uuid4()), 379 | 'type': id_token_type 380 | }, 381 | remote_start_id=remote_start_id 382 | ) 383 | response = await self.call(request) 384 | return response 385 | 386 | async def send_stop_transaction(self): 387 | transaction_id = typer.prompt('Enter transaction ID', default=fake.pystr(1, 12)) 388 | request = call.RequestStopTransactionPayload( 389 | transaction_id=transaction_id 390 | ) 391 | response = await self.call(request) 392 | return response 393 | 394 | async def send_reservation_status_update(self): 395 | reservation_id = typer.prompt('Enter reservation ID', default=fake.pyint(1, 100)) 396 | reservation_update_status = await ask_question(enums.ReservationUpdateStatusType, 397 | "Which reservation update status:") 398 | request = call.ReservationStatusUpdatePayload( 399 | reservation_id=reservation_id, 400 | reservation_update_status=reservation_update_status 401 | ) 402 | response = await self.call(request) 403 | return response 404 | 405 | async def send_security_event_notification(self): 406 | security_event_type = await ask_question([ 407 | 'FirmwareUpdated', 408 | 'FailedToAuthenticateAtCsms', 409 | 'CsmsFailedToAuthenticate', 410 | 'SettingSystemTime', 411 | 'StartupOfTheDevice', 412 | 'ResetOrReboot', 413 | 'SecurityLogWasCleared', 414 | 'ReconfigurationOfSecurityParameters', 415 | 'MemoryExhaustion', 416 | 'InvalidMessages', 417 | 'AttemptedReplayAttacks', 418 | 'TamperDetectionActivated', 419 | 'InvalidFirmwareSignature', 420 | 'InvalidFirmwareSigningCertificate', 421 | 'InvalidCsmsCertificate', 422 | 'InvalidChargingStationCertificate', 423 | 'InvalidTLSVersion', 424 | 'InvalidTLSCipherSuite' 425 | ], "Which reservation update status: ") 426 | request = call.SecurityEventNotificationPayload( 427 | type=security_event_type, 428 | timestamp=str(datetime.now()) 429 | ) 430 | response = await self.call(request) 431 | return response 432 | 433 | async def send_sign_certificate(self): 434 | csr = typer.prompt('Enter Certificate Signing Request', fake.pystr(1, 100)) 435 | request = call.SignCertificatePayload( 436 | csr=csr 437 | ) 438 | response = await self.call(request) 439 | return response 440 | 441 | async def send_transaction_event(self): 442 | transaction_id = typer.prompt('Enter transaction ID', default=fake.pystr(1, 36)) 443 | seq_no = typer.prompt('Enter sequence number', default=fake.pyint(1, 100)) 444 | event_type = await ask_question(enums.TransactionEventType, "Which transaction event type:") 445 | trigger_reason = await ask_question(enums.TriggerReasonType, "Which trigger reason:") 446 | request = call.TransactionEventPayload( 447 | event_type=event_type, 448 | timestamp=str(datetime.now()), 449 | trigger_reason=trigger_reason, 450 | seq_no=seq_no, 451 | transaction_info=datatypes.TransactionType( 452 | transaction_id=transaction_id 453 | ) 454 | 455 | ) 456 | response = await self.call(request) 457 | return response 458 | 459 | @on(enums.Action.CancelReservation) 460 | async def on_cancel_reservation(self, status=enums.CancelReservationStatusType.accepted, **kwargs): 461 | return call_result.CancelReservationPayload(status=status) 462 | 463 | @on(enums.Action.CertificateSigned) 464 | async def on_certificate_signed(self, status=enums.CertificateSignedStatusType.accepted, **kwargs): 465 | return call_result.CertificateSignedPayload(status=status) 466 | 467 | @on(enums.Action.ChangeAvailability) 468 | async def on_change_availability(self, status=enums.ChangeAvailabilityStatusType.accepted, **kwargs): 469 | return call_result.ChangeAvailabilityPayload(status=status) 470 | 471 | @on(enums.Action.ClearChargingProfile) 472 | async def on_clear_charging_profile(self, status=enums.ClearChargingProfileStatusType.accepted, **kwargs): 473 | return call_result.ClearChargingProfilePayload(status=status) 474 | 475 | @on(enums.Action.ClearDisplayMessage) 476 | async def on_clear_display_message(self, status=enums.ClearChargingProfileStatusType.accepted, **kwargs): 477 | return call_result.ClearDisplayMessagePayload(status=status) 478 | 479 | @on(enums.Action.ClearVariableMonitoring) 480 | async def on_clear_variable_monitoring(self, monitoring_result_id: int = fake.random_int(1, 100), 481 | status=enums.ClearMonitoringStatusType.accepted, **kwargs): 482 | return call_result.ClearVariableMonitoringPayload( 483 | clear_monitoring_result=[{ 484 | 'status': status, 485 | 'id': monitoring_result_id, 486 | }] 487 | ) 488 | 489 | @on('CostUpdated') 490 | async def on_cost_updated(self, **kwargs): 491 | return call_result.CostUpdatedPayload() 492 | 493 | @on(enums.Action.CustomerInformation) 494 | async def on_customer_info(self, status=enums.CustomerInformationStatusType.accepted, **kwargs): 495 | return call_result.CustomerInformationPayload(status=status) 496 | 497 | @on(enums.Action.DataTransfer) 498 | async def on_data_transfer(self, status=enums.DataTransferStatusType.accepted, **kwargs): 499 | return call_result.DataTransferPayload(status=status) 500 | 501 | @on(enums.Action.DeleteCertificate) 502 | async def on_delete_certificate(self, status=enums.DeleteCertificateStatusType.accepted, **kwargs): 503 | return call_result.DeleteCertificatePayload(status=status) 504 | 505 | @on(enums.Action.GetBaseReport) 506 | async def on_get_base_report(self, status=enums.GenericDeviceModelStatusType.accepted, **kwargs): 507 | return call_result.GetBaseReportPayload(status=status) 508 | 509 | @on(enums.Action.GetChargingProfiles) 510 | async def on_get_charging_profiles(self, status=enums.GetChargingProfileStatusType.accepted, **kwargs): 511 | return call_result.GetChargingProfilesPayload(status=status) 512 | 513 | @on(enums.Action.GetCompositeSchedule) 514 | async def on_get_composite_schedule(self, status=enums.GenericStatusType.accepted, **kwargs): 515 | return call_result.GetCompositeSchedulePayload(status=status) 516 | 517 | @on(enums.Action.GetDisplayMessages) 518 | async def on_get_display_messages(self, status=enums.GetDisplayMessagesStatusType.accepted, **kwargs): 519 | return call_result.GetDisplayMessagesPayload(status=status) 520 | 521 | @on(enums.Action.GetInstalledCertificateIds) 522 | async def on_get_installed_certificate_ids(self, status=enums.GetInstalledCertificateStatusType.accepted, **kwargs): 523 | return call_result.GetInstalledCertificateIdsPayload(status=status) 524 | 525 | @on(enums.Action.GetLocalListVersion) 526 | async def on_get_local_list_version(self, version_number: int = fake.random_int(1, 1000), **kwargs): 527 | return call_result.GetLocalListVersionPayload(version_number=version_number) 528 | 529 | @on(enums.Action.GetLog) 530 | async def on_get_log(self, status=enums.LogStatusType.accepted, **kwargs): 531 | return call_result.GetLogPayload(status=status) 532 | 533 | @on(enums.Action.GetMonitoringReport) 534 | async def on_get_monitoring_report(self, status=enums.InstallCertificateStatusType.accepted, **kwargs): 535 | return call_result.GetMonitoringReportPayload(status=status) 536 | 537 | @on(enums.Action.PublishFirmware) 538 | async def on_publish_firmware(self, status=enums.GenericStatusType.accepted, **kwargs): 539 | return call_result.PublishFirmwarePayload(status=status) 540 | 541 | @on(enums.Action.ReserveNow) 542 | async def on_reserve_now(self, status=enums.ReserveNowStatusType.accepted, **kwargs): 543 | return call_result.ReserveNowPayload(status=status) 544 | 545 | @on(enums.Action.Reset) 546 | async def on_reset(self, status=enums.ResetStatusType.accepted, **kwargs): 547 | return call_result.ResetPayload(status=status) 548 | 549 | @on(enums.Action.SendLocalList) 550 | async def on_send_local_list(self, status=enums.SendLocalListStatusType.accepted, **kwargs): 551 | return call_result.SendLocalListPayload(status=status) 552 | 553 | @on(enums.Action.SetChargingProfile) 554 | async def on_set_charging_profile(self, status=enums.ChargingProfileStatus.accepted, **kwargs): 555 | return call_result.SetChargingProfilePayload(status=status) 556 | 557 | @on(enums.Action.SetDisplayMessage) 558 | async def on_set_display_message(self, status=enums.DisplayMessageStatusType.accepted, **kwargs): 559 | return call_result.SetDisplayMessagePayload(status=status) 560 | 561 | @on(enums.Action.SetMonitoringBase) 562 | async def on_set_monitoring_base(self, status=enums.GenericDeviceModelStatusType.accepted, **kwargs): 563 | return call_result.SetMonitoringBasePayload(status=status) 564 | 565 | @on(enums.Action.SetMonitoringLevel) 566 | async def on_set_monitoring_level(self, status=enums.GenericStatusType.accepted, **kwargs): 567 | return call_result.SetMonitoringLevelPayload(status=status) 568 | 569 | @on(enums.Action.SetNetworkProfile) 570 | async def on_set_network_profile(self, status=enums.SetNetworkProfileStatusType.accepted, **kwargs): 571 | return call_result.SetNetworkProfilePayload(status=status) 572 | 573 | @on(enums.Action.SetVariableMonitoring) 574 | async def on_set_variable_monitoring(self, status=enums.SetMonitoringStatusType.accepted, 575 | monitor_type=enums.MonitorType.delta, severity: int = fake.random_int(0, 10), 576 | component_name: str = fake.pystr(1, 36), 577 | component_instance: str = fake.pystr(1, 36), 578 | variable_name: str = fake.pystr(1, 36), **kwargs): 579 | return call_result.SetVariableMonitoringPayload( 580 | set_monitoring_result=[datatypes.SetMonitoringResultType( 581 | status=status, 582 | type=monitor_type, 583 | severity=severity, 584 | component=datatypes.ComponentType( 585 | name=component_name, 586 | instance=component_instance 587 | ), 588 | variable=datatypes.VariableType( 589 | name=variable_name 590 | ) 591 | )] 592 | ) 593 | 594 | @on(enums.Action.SetVariables) 595 | async def on_set_variables(self, status=enums.SetVariableStatusType.accepted, 596 | component_name: str = fake.pystr(1, 36), 597 | component_instance: str = fake.pystr(1, 36), 598 | variable_name: str = fake.pystr(1, 36), **kwargs): 599 | return call_result.SetVariablesPayload( 600 | set_variable_result=[datatypes.SetVariableResultType( 601 | attribute_status=status, 602 | component=datatypes.ComponentType( 603 | name=component_name, 604 | instance=component_instance 605 | ), 606 | variable=datatypes.VariableType( 607 | name=variable_name 608 | ) 609 | )] 610 | ) 611 | 612 | @on(enums.Action.TriggerMessage) 613 | async def on_trigger_message(self, status=enums.TriggerMessageStatusType.accepted, **kwargs): 614 | return call_result.TriggerMessagePayload(status=status) 615 | 616 | @on(enums.Action.UnlockConnector) 617 | async def on_unlock_connector(self, status=enums.UnlockStatusType.unlocked, **kwargs): 618 | return call_result.UnlockConnectorPayload(status=status) 619 | 620 | @on(enums.Action.UnpublishFirmware) 621 | async def on_unpublish_firmware(self, status=enums.UnpublishFirmwareStatusType.unpublished, **kwargs): 622 | return call_result.UnpublishFirmwarePayload(status=status) 623 | 624 | @on(enums.Action.UpdateFirmware) 625 | async def on_update_firmware(self, status=enums.UpdateFirmwareStatusType.accepted, **kwargs): 626 | return call_result.UpdateFirmwarePayload(status=status) 627 | 628 | messages = { 629 | 'Authorize': send_authorize, 630 | 'Clear cache': send_clear_cache, 631 | 'Cleared Charging Limit': send_cleared_charging_request, 632 | 'Firmware Status Notification': send_firmware_status_notification, 633 | 'Get 15118 EV Certificate': send_get_15118ev_certificate, 634 | 'Get Certificate Status': send_get_certificate_status, 635 | 'Get Display Messages': send_get_display_messages, 636 | 'Log Status Notification': send_log_status_notification, 637 | 'Meter Value': send_meter_value, 638 | 'Notify Charging Limit': send_notify_charging_limit, 639 | 'Notify Customer Information': send_notify_customer_information, 640 | 'Notify Display Messages': send_notify_display_messages, 641 | 'Notify EV Charging Needs': send_notify_ev_charging_needs, 642 | 'Notify EV Charging Schedule': send_notify_ev_charging_schedule, 643 | 'Notify Event': send_notify_event, 644 | 'Notify Monitoring Report': send_notify_monitoring_report, 645 | 'Notify Report': send_notify_report, 646 | 'Publish Firmware Status': send_publish_firmware_status_notification, 647 | 'Report Charging Profiles': send_report_charging_profiles, 648 | 'Heartbeat': send_heartbeat, 649 | 'Start transaction': send_start_transaction, 650 | 'Stop transaction': send_stop_transaction, 651 | 'Reservation Status Update': send_reservation_status_update, 652 | 'Security Event Notification': send_security_event_notification, 653 | 'Sign Certificate': send_sign_certificate, 654 | 'Transaction Event': send_transaction_event 655 | 656 | } 657 | 658 | 659 | # Part below this line is used to test messages send form central system to charge point 660 | async def on_connect(websocket, path): 661 | """ For every new charge point that connects, create a ChargePoint 662 | instance and start listening for messages. 663 | """ 664 | try: 665 | requested_protocols = websocket.request_headers[ 666 | 'Sec-WebSocket-Protocol'] 667 | except KeyError: 668 | logging.info("Client hasn't requested any Subprotocol. " 669 | "Closing Connection") 670 | if websocket.subprotocol: 671 | logging.info("Protocols Matched: %s", websocket.subprotocol) 672 | else: 673 | # In the websockets lib if no subprotocols are supported by the 674 | # client and the server, it proceeds without a subprotocol, 675 | # so we have to manually close the connection. 676 | logging.warning('Protocols Mismatched | Expected Subprotocols: %s,' 677 | ' but client supports %s | Closing connection', 678 | websocket.available_subprotocols, 679 | requested_protocols) 680 | return await websocket.close() 681 | 682 | charge_point_id = path.strip('/') 683 | cp = ChargePoint(charge_point_id, websocket) 684 | 685 | await cp.start() 686 | 687 | 688 | async def start_cp(): # nosec 689 | server = await websockets.serve( 690 | on_connect, 691 | "0.0.0.0", 692 | 9000, 693 | subprotocols=['ocpp2.0.1'] 694 | ) 695 | logging.info("WebSocket Server Started") 696 | return server 697 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "astroid" 3 | version = "2.11.7" 4 | description = "An abstract syntax tree for Python with inference support." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.6.2" 8 | 9 | [package.dependencies] 10 | lazy-object-proxy = ">=1.4.0" 11 | typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} 12 | wrapt = ">=1.11,<2" 13 | 14 | [[package]] 15 | name = "atomicwrites" 16 | version = "1.4.1" 17 | description = "Atomic file writes." 18 | category = "main" 19 | optional = false 20 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 21 | 22 | [[package]] 23 | name = "attrs" 24 | version = "22.1.0" 25 | description = "Classes Without Boilerplate" 26 | category = "main" 27 | optional = false 28 | python-versions = ">=3.5" 29 | 30 | [package.extras] 31 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 32 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 33 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 34 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] 35 | 36 | [[package]] 37 | name = "bandit" 38 | version = "1.7.4" 39 | description = "Security oriented static analyser for python code." 40 | category = "dev" 41 | optional = false 42 | python-versions = ">=3.7" 43 | 44 | [package.dependencies] 45 | colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} 46 | GitPython = ">=1.0.1" 47 | PyYAML = ">=5.3.1" 48 | stevedore = ">=1.20.0" 49 | 50 | [package.extras] 51 | test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "toml", "beautifulsoup4 (>=4.8.0)", "pylint (==1.9.4)"] 52 | toml = ["toml"] 53 | yaml = ["pyyaml"] 54 | 55 | [[package]] 56 | name = "click" 57 | version = "8.1.3" 58 | description = "Composable command line interface toolkit" 59 | category = "main" 60 | optional = false 61 | python-versions = ">=3.7" 62 | 63 | [package.dependencies] 64 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 65 | 66 | [[package]] 67 | name = "colorama" 68 | version = "0.4.5" 69 | description = "Cross-platform colored terminal text." 70 | category = "main" 71 | optional = false 72 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 73 | 74 | [[package]] 75 | name = "coverage" 76 | version = "6.4.4" 77 | description = "Code coverage measurement for Python" 78 | category = "dev" 79 | optional = false 80 | python-versions = ">=3.7" 81 | 82 | [package.extras] 83 | toml = ["tomli"] 84 | 85 | [[package]] 86 | name = "dill" 87 | version = "0.3.5.1" 88 | description = "serialize all of python" 89 | category = "dev" 90 | optional = false 91 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" 92 | 93 | [package.extras] 94 | graph = ["objgraph (>=1.7.2)"] 95 | 96 | [[package]] 97 | name = "dodgy" 98 | version = "0.2.1" 99 | description = "Dodgy: Searches for dodgy looking lines in Python code" 100 | category = "dev" 101 | optional = false 102 | python-versions = "*" 103 | 104 | [[package]] 105 | name = "factory-boy" 106 | version = "3.2.1" 107 | description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." 108 | category = "main" 109 | optional = false 110 | python-versions = ">=3.6" 111 | 112 | [package.dependencies] 113 | Faker = ">=0.7.0" 114 | 115 | [package.extras] 116 | dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"] 117 | doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] 118 | 119 | [[package]] 120 | name = "faker" 121 | version = "13.16.0" 122 | description = "Faker is a Python package that generates fake data for you." 123 | category = "main" 124 | optional = false 125 | python-versions = ">=3.6" 126 | 127 | [package.dependencies] 128 | python-dateutil = ">=2.4" 129 | 130 | [[package]] 131 | name = "flake8" 132 | version = "2.3.0" 133 | description = "the modular source code checker: pep8, pyflakes and co" 134 | category = "dev" 135 | optional = false 136 | python-versions = "*" 137 | 138 | [package.dependencies] 139 | mccabe = ">=0.2.1" 140 | pep8 = ">=1.5.7" 141 | pyflakes = ">=0.8.1" 142 | 143 | [[package]] 144 | name = "flake8-polyfill" 145 | version = "1.0.2" 146 | description = "Polyfill package for Flake8 plugins" 147 | category = "dev" 148 | optional = false 149 | python-versions = "*" 150 | 151 | [package.dependencies] 152 | flake8 = "*" 153 | 154 | [[package]] 155 | name = "gitdb" 156 | version = "4.0.9" 157 | description = "Git Object Database" 158 | category = "dev" 159 | optional = false 160 | python-versions = ">=3.6" 161 | 162 | [package.dependencies] 163 | smmap = ">=3.0.1,<6" 164 | 165 | [[package]] 166 | name = "gitpython" 167 | version = "3.1.27" 168 | description = "GitPython is a python library used to interact with Git repositories" 169 | category = "dev" 170 | optional = false 171 | python-versions = ">=3.7" 172 | 173 | [package.dependencies] 174 | gitdb = ">=4.0.1,<5" 175 | 176 | [[package]] 177 | name = "iniconfig" 178 | version = "1.1.1" 179 | description = "iniconfig: brain-dead simple config-ini parsing" 180 | category = "main" 181 | optional = false 182 | python-versions = "*" 183 | 184 | [[package]] 185 | name = "isort" 186 | version = "5.10.1" 187 | description = "A Python utility / library to sort Python imports." 188 | category = "dev" 189 | optional = false 190 | python-versions = ">=3.6.1,<4.0" 191 | 192 | [package.extras] 193 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 194 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 195 | colors = ["colorama (>=0.4.3,<0.5.0)"] 196 | plugins = ["setuptools"] 197 | 198 | [[package]] 199 | name = "jsonschema" 200 | version = "4.14.0" 201 | description = "An implementation of JSON Schema validation for Python" 202 | category = "main" 203 | optional = false 204 | python-versions = ">=3.7" 205 | 206 | [package.dependencies] 207 | attrs = ">=17.4.0" 208 | pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" 209 | 210 | [package.extras] 211 | format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] 212 | format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] 213 | 214 | [[package]] 215 | name = "lazy-object-proxy" 216 | version = "1.7.1" 217 | description = "A fast and thorough lazy object proxy." 218 | category = "dev" 219 | optional = false 220 | python-versions = ">=3.6" 221 | 222 | [[package]] 223 | name = "mccabe" 224 | version = "0.6.1" 225 | description = "McCabe checker, plugin for flake8" 226 | category = "dev" 227 | optional = false 228 | python-versions = "*" 229 | 230 | [[package]] 231 | name = "ocpp" 232 | version = "0.15.0" 233 | description = "Python package implementing the JSON version of the Open Charge Point Protocol (OCPP)." 234 | category = "main" 235 | optional = false 236 | python-versions = ">=3.7,<4.0" 237 | 238 | [package.dependencies] 239 | jsonschema = ">=4.4.0,<5.0.0" 240 | 241 | [[package]] 242 | name = "packaging" 243 | version = "21.3" 244 | description = "Core utilities for Python packages" 245 | category = "main" 246 | optional = false 247 | python-versions = ">=3.6" 248 | 249 | [package.dependencies] 250 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 251 | 252 | [[package]] 253 | name = "pbr" 254 | version = "5.10.0" 255 | description = "Python Build Reasonableness" 256 | category = "dev" 257 | optional = false 258 | python-versions = ">=2.6" 259 | 260 | [[package]] 261 | name = "pep8" 262 | version = "1.7.1" 263 | description = "Python style guide checker" 264 | category = "dev" 265 | optional = false 266 | python-versions = "*" 267 | 268 | [[package]] 269 | name = "pep8-naming" 270 | version = "0.10.0" 271 | description = "Check PEP-8 naming conventions, plugin for flake8" 272 | category = "dev" 273 | optional = false 274 | python-versions = "*" 275 | 276 | [package.dependencies] 277 | flake8-polyfill = ">=1.0.2,<2" 278 | 279 | [[package]] 280 | name = "platformdirs" 281 | version = "2.5.2" 282 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 283 | category = "dev" 284 | optional = false 285 | python-versions = ">=3.7" 286 | 287 | [package.extras] 288 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 289 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 290 | 291 | [[package]] 292 | name = "pluggy" 293 | version = "1.0.0" 294 | description = "plugin and hook calling mechanisms for python" 295 | category = "main" 296 | optional = false 297 | python-versions = ">=3.6" 298 | 299 | [package.extras] 300 | dev = ["pre-commit", "tox"] 301 | testing = ["pytest", "pytest-benchmark"] 302 | 303 | [[package]] 304 | name = "prompt-toolkit" 305 | version = "3.0.30" 306 | description = "Library for building powerful interactive command lines in Python" 307 | category = "main" 308 | optional = false 309 | python-versions = ">=3.6.2" 310 | 311 | [package.dependencies] 312 | wcwidth = "*" 313 | 314 | [[package]] 315 | name = "prospector" 316 | version = "1.7.7" 317 | description = "" 318 | category = "dev" 319 | optional = false 320 | python-versions = ">=3.6.2,<4.0" 321 | 322 | [package.dependencies] 323 | dodgy = ">=0.2.1,<0.3.0" 324 | mccabe = ">=0.6.0,<0.7.0" 325 | pep8-naming = ">=0.3.3,<=0.10.0" 326 | pycodestyle = ">=2.6.0,<2.9.0" 327 | pydocstyle = ">=2.0.0" 328 | pyflakes = ">=2.2.0,<3" 329 | pylint = ">=2.8.3" 330 | pylint-celery = "0.3" 331 | pylint-django = ">=2.5,<2.6" 332 | pylint-flask = "0.6" 333 | pylint-plugin-utils = ">=0.7,<0.8" 334 | PyYAML = "*" 335 | requirements-detector = ">=0.7,<0.8" 336 | setoptconf-tmp = ">=0.3.1,<0.4.0" 337 | toml = ">=0.10.2,<0.11.0" 338 | 339 | [package.extras] 340 | with_bandit = ["bandit (>=1.5.1)"] 341 | with_everything = ["bandit (>=1.5.1)", "frosted (>=1.4.1)", "mypy (>=0.600)", "pyroma (>=2.4)", "vulture (>=1.5)"] 342 | with_frosted = ["frosted (>=1.4.1)"] 343 | with_mypy = ["mypy (>=0.600)"] 344 | with_pyroma = ["pyroma (>=2.4)"] 345 | with_vulture = ["vulture (>=1.5)"] 346 | 347 | [[package]] 348 | name = "py" 349 | version = "1.11.0" 350 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 351 | category = "main" 352 | optional = false 353 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 354 | 355 | [[package]] 356 | name = "pycodestyle" 357 | version = "2.8.0" 358 | description = "Python style guide checker" 359 | category = "dev" 360 | optional = false 361 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 362 | 363 | [[package]] 364 | name = "pydocstyle" 365 | version = "6.1.1" 366 | description = "Python docstring style checker" 367 | category = "dev" 368 | optional = false 369 | python-versions = ">=3.6" 370 | 371 | [package.dependencies] 372 | snowballstemmer = "*" 373 | 374 | [package.extras] 375 | toml = ["toml"] 376 | 377 | [[package]] 378 | name = "pyflakes" 379 | version = "2.5.0" 380 | description = "passive checker of Python programs" 381 | category = "dev" 382 | optional = false 383 | python-versions = ">=3.6" 384 | 385 | [[package]] 386 | name = "pylint" 387 | version = "2.14.5" 388 | description = "python code static checker" 389 | category = "dev" 390 | optional = false 391 | python-versions = ">=3.7.2" 392 | 393 | [package.dependencies] 394 | astroid = ">=2.11.6,<=2.12.0-dev0" 395 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 396 | dill = ">=0.2" 397 | isort = ">=4.2.5,<6" 398 | mccabe = ">=0.6,<0.8" 399 | platformdirs = ">=2.2.0" 400 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 401 | tomlkit = ">=0.10.1" 402 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 403 | 404 | [package.extras] 405 | spelling = ["pyenchant (>=3.2,<4.0)"] 406 | testutils = ["gitpython (>3)"] 407 | 408 | [[package]] 409 | name = "pylint-celery" 410 | version = "0.3" 411 | description = "pylint-celery is a Pylint plugin to aid Pylint in recognising and understandingerrors caused when using the Celery library" 412 | category = "dev" 413 | optional = false 414 | python-versions = "*" 415 | 416 | [package.dependencies] 417 | astroid = ">=1.0" 418 | pylint = ">=1.0" 419 | pylint-plugin-utils = ">=0.2.1" 420 | 421 | [[package]] 422 | name = "pylint-django" 423 | version = "2.5.3" 424 | description = "A Pylint plugin to help Pylint understand the Django web framework" 425 | category = "dev" 426 | optional = false 427 | python-versions = "*" 428 | 429 | [package.dependencies] 430 | pylint = ">=2.0,<3" 431 | pylint-plugin-utils = ">=0.7" 432 | 433 | [package.extras] 434 | for_tests = ["django-tables2", "factory-boy", "coverage", "pytest", "wheel", "django-tastypie", "pylint (>=2.13)"] 435 | with_django = ["django"] 436 | 437 | [[package]] 438 | name = "pylint-flask" 439 | version = "0.6" 440 | description = "pylint-flask is a Pylint plugin to aid Pylint in recognizing and understanding errors caused when using Flask" 441 | category = "dev" 442 | optional = false 443 | python-versions = "*" 444 | 445 | [package.dependencies] 446 | pylint-plugin-utils = ">=0.2.1" 447 | 448 | [[package]] 449 | name = "pylint-plugin-utils" 450 | version = "0.7" 451 | description = "Utilities and helpers for writing Pylint plugins" 452 | category = "dev" 453 | optional = false 454 | python-versions = ">=3.6.2" 455 | 456 | [package.dependencies] 457 | pylint = ">=1.7" 458 | 459 | [[package]] 460 | name = "pyparsing" 461 | version = "3.0.9" 462 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 463 | category = "main" 464 | optional = false 465 | python-versions = ">=3.6.8" 466 | 467 | [package.extras] 468 | diagrams = ["railroad-diagrams", "jinja2"] 469 | 470 | [[package]] 471 | name = "pyrsistent" 472 | version = "0.18.1" 473 | description = "Persistent/Functional/Immutable data structures" 474 | category = "main" 475 | optional = false 476 | python-versions = ">=3.7" 477 | 478 | [[package]] 479 | name = "pytest" 480 | version = "7.1.2" 481 | description = "pytest: simple powerful testing with Python" 482 | category = "main" 483 | optional = false 484 | python-versions = ">=3.7" 485 | 486 | [package.dependencies] 487 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 488 | attrs = ">=19.2.0" 489 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 490 | iniconfig = "*" 491 | packaging = "*" 492 | pluggy = ">=0.12,<2.0" 493 | py = ">=1.8.2" 494 | tomli = ">=1.0.0" 495 | 496 | [package.extras] 497 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 498 | 499 | [[package]] 500 | name = "pytest-asyncio" 501 | version = "0.19.0" 502 | description = "Pytest support for asyncio" 503 | category = "main" 504 | optional = false 505 | python-versions = ">=3.7" 506 | 507 | [package.dependencies] 508 | pytest = ">=6.1.0" 509 | 510 | [package.extras] 511 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] 512 | 513 | [[package]] 514 | name = "pytest-cov" 515 | version = "2.12.1" 516 | description = "Pytest plugin for measuring coverage." 517 | category = "dev" 518 | optional = false 519 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 520 | 521 | [package.dependencies] 522 | coverage = ">=5.2.1" 523 | pytest = ">=4.6" 524 | toml = "*" 525 | 526 | [package.extras] 527 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 528 | 529 | [[package]] 530 | name = "python-dateutil" 531 | version = "2.8.2" 532 | description = "Extensions to the standard Python datetime module" 533 | category = "main" 534 | optional = false 535 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 536 | 537 | [package.dependencies] 538 | six = ">=1.5" 539 | 540 | [[package]] 541 | name = "pyyaml" 542 | version = "6.0" 543 | description = "YAML parser and emitter for Python" 544 | category = "dev" 545 | optional = false 546 | python-versions = ">=3.6" 547 | 548 | [[package]] 549 | name = "questionary" 550 | version = "1.10.0" 551 | description = "Python library to build pretty command line user prompts ⭐️" 552 | category = "main" 553 | optional = false 554 | python-versions = ">=3.6,<4.0" 555 | 556 | [package.dependencies] 557 | prompt_toolkit = ">=2.0,<4.0" 558 | 559 | [package.extras] 560 | docs = ["Sphinx (>=3.3,<4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)"] 561 | 562 | [[package]] 563 | name = "requirements-detector" 564 | version = "0.7" 565 | description = "Python tool to find and list requirements of a Python project" 566 | category = "dev" 567 | optional = false 568 | python-versions = "*" 569 | 570 | [package.dependencies] 571 | astroid = ">=1.4" 572 | 573 | [[package]] 574 | name = "setoptconf-tmp" 575 | version = "0.3.1" 576 | description = "A module for retrieving program settings from various sources in a consistant method." 577 | category = "dev" 578 | optional = false 579 | python-versions = "*" 580 | 581 | [package.extras] 582 | yaml = ["pyyaml"] 583 | 584 | [[package]] 585 | name = "six" 586 | version = "1.16.0" 587 | description = "Python 2 and 3 compatibility utilities" 588 | category = "main" 589 | optional = false 590 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 591 | 592 | [[package]] 593 | name = "smmap" 594 | version = "5.0.0" 595 | description = "A pure Python implementation of a sliding window memory map manager" 596 | category = "dev" 597 | optional = false 598 | python-versions = ">=3.6" 599 | 600 | [[package]] 601 | name = "snowballstemmer" 602 | version = "2.2.0" 603 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 604 | category = "dev" 605 | optional = false 606 | python-versions = "*" 607 | 608 | [[package]] 609 | name = "stevedore" 610 | version = "4.0.0" 611 | description = "Manage dynamic plugins for Python applications" 612 | category = "dev" 613 | optional = false 614 | python-versions = ">=3.8" 615 | 616 | [package.dependencies] 617 | pbr = ">=2.0.0,<2.1.0 || >2.1.0" 618 | 619 | [[package]] 620 | name = "toml" 621 | version = "0.10.2" 622 | description = "Python Library for Tom's Obvious, Minimal Language" 623 | category = "dev" 624 | optional = false 625 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 626 | 627 | [[package]] 628 | name = "tomli" 629 | version = "2.0.1" 630 | description = "A lil' TOML parser" 631 | category = "main" 632 | optional = false 633 | python-versions = ">=3.7" 634 | 635 | [[package]] 636 | name = "tomlkit" 637 | version = "0.11.4" 638 | description = "Style preserving TOML library" 639 | category = "dev" 640 | optional = false 641 | python-versions = ">=3.6,<4.0" 642 | 643 | [[package]] 644 | name = "typer" 645 | version = "0.6.1" 646 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 647 | category = "main" 648 | optional = false 649 | python-versions = ">=3.6" 650 | 651 | [package.dependencies] 652 | click = ">=7.1.1,<9.0.0" 653 | 654 | [package.extras] 655 | test = ["rich (>=10.11.0,<13.0.0)", "isort (>=5.0.6,<6.0.0)", "black (>=22.3.0,<23.0.0)", "mypy (==0.910)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "coverage (>=5.2,<6.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest (>=4.4.0,<5.4.0)", "shellingham (>=1.3.0,<2.0.0)"] 656 | doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] 657 | dev = ["pre-commit (>=2.17.0,<3.0.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.3.1,<2.0.0)"] 658 | all = ["rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)", "colorama (>=0.4.3,<0.5.0)"] 659 | 660 | [[package]] 661 | name = "typing-extensions" 662 | version = "4.3.0" 663 | description = "Backported and Experimental Type Hints for Python 3.7+" 664 | category = "dev" 665 | optional = false 666 | python-versions = ">=3.7" 667 | 668 | [[package]] 669 | name = "wcwidth" 670 | version = "0.2.5" 671 | description = "Measures the displayed width of unicode strings in a terminal" 672 | category = "main" 673 | optional = false 674 | python-versions = "*" 675 | 676 | [[package]] 677 | name = "websockets" 678 | version = "10.3" 679 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 680 | category = "main" 681 | optional = false 682 | python-versions = ">=3.7" 683 | 684 | [[package]] 685 | name = "wrapt" 686 | version = "1.14.1" 687 | description = "Module for decorators, wrappers and monkey patching." 688 | category = "dev" 689 | optional = false 690 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 691 | 692 | [metadata] 693 | lock-version = "1.1" 694 | python-versions = "^3.9" 695 | content-hash = "67a8d69d9ad03e91ce6d5876d8868b5d90e96de7c57b0f40be673dc431ac590f" 696 | 697 | [metadata.files] 698 | astroid = [] 699 | atomicwrites = [] 700 | attrs = [] 701 | bandit = [ 702 | {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"}, 703 | {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"}, 704 | ] 705 | click = [ 706 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 707 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 708 | ] 709 | colorama = [ 710 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 711 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 712 | ] 713 | coverage = [ 714 | {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, 715 | {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, 716 | {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, 717 | {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, 718 | {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, 719 | {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, 720 | {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, 721 | {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, 722 | {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, 723 | {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, 724 | {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, 725 | {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, 726 | {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, 727 | {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, 728 | {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, 729 | {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, 730 | {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, 731 | {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, 732 | {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, 733 | {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, 734 | {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, 735 | {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, 736 | {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, 737 | {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, 738 | {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, 739 | {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, 740 | {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, 741 | {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, 742 | {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, 743 | {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, 744 | {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, 745 | {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, 746 | {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, 747 | {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, 748 | {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, 749 | {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, 750 | {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, 751 | {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, 752 | {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, 753 | {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, 754 | {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, 755 | {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, 756 | {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, 757 | {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, 758 | {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, 759 | {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, 760 | {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, 761 | {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, 762 | {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, 763 | {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, 764 | ] 765 | dill = [ 766 | {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, 767 | {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, 768 | ] 769 | dodgy = [ 770 | {file = "dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"}, 771 | {file = "dodgy-0.2.1.tar.gz", hash = "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a"}, 772 | ] 773 | factory-boy = [ 774 | {file = "factory_boy-3.2.1-py2.py3-none-any.whl", hash = "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795"}, 775 | {file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"}, 776 | ] 777 | faker = [ 778 | {file = "Faker-13.16.0-py3-none-any.whl", hash = "sha256:920f94d5aa865fd922bc29f2cf75c75b4d86b30eec23e7174d7513241b759b05"}, 779 | {file = "Faker-13.16.0.tar.gz", hash = "sha256:25c5be99bc5fd8676eea8c1490e0de87f6d9734651c7af2cefc99b322b2936f4"}, 780 | ] 781 | flake8 = [] 782 | flake8-polyfill = [ 783 | {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, 784 | {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, 785 | ] 786 | gitdb = [ 787 | {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, 788 | {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, 789 | ] 790 | gitpython = [ 791 | {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"}, 792 | {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, 793 | ] 794 | iniconfig = [ 795 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 796 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 797 | ] 798 | isort = [ 799 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 800 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 801 | ] 802 | jsonschema = [ 803 | {file = "jsonschema-4.14.0-py3-none-any.whl", hash = "sha256:9892b8d630a82990521a9ca630d3446bd316b5ad54dbe981338802787f3e0d2d"}, 804 | {file = "jsonschema-4.14.0.tar.gz", hash = "sha256:15062f4cc6f591400cd528d2c355f2cfa6a57e44c820dc783aee5e23d36a831f"}, 805 | ] 806 | lazy-object-proxy = [ 807 | {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, 808 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, 809 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, 810 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, 811 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, 812 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, 813 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, 814 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, 815 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, 816 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, 817 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, 818 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, 819 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, 820 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, 821 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, 822 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, 823 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, 824 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, 825 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, 826 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, 827 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, 828 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, 829 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, 830 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, 831 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, 832 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, 833 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, 834 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, 835 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, 836 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, 837 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, 838 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, 839 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, 840 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, 841 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, 842 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, 843 | {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, 844 | ] 845 | mccabe = [ 846 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 847 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 848 | ] 849 | ocpp = [ 850 | {file = "ocpp-0.15.0-py3-none-any.whl", hash = "sha256:0a3c68a2e14d74cdc46693c9e7524bbf5171391e73411a46506d32080291e51d"}, 851 | {file = "ocpp-0.15.0.tar.gz", hash = "sha256:552df698e093fc6a400da211ed91aed99ae5dfb6361ec2a1f92b2a763f686985"}, 852 | ] 853 | packaging = [ 854 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 855 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 856 | ] 857 | pbr = [ 858 | {file = "pbr-5.10.0-py2.py3-none-any.whl", hash = "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf"}, 859 | {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"}, 860 | ] 861 | pep8 = [] 862 | pep8-naming = [ 863 | {file = "pep8-naming-0.10.0.tar.gz", hash = "sha256:f3b4a5f9dd72b991bf7d8e2a341d2e1aa3a884a769b5aaac4f56825c1763bf3a"}, 864 | {file = "pep8_naming-0.10.0-py2.py3-none-any.whl", hash = "sha256:5d9f1056cb9427ce344e98d1a7f5665710e2f20f748438e308995852cfa24164"}, 865 | ] 866 | platformdirs = [ 867 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 868 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 869 | ] 870 | pluggy = [ 871 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 872 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 873 | ] 874 | prompt-toolkit = [ 875 | {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, 876 | {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, 877 | ] 878 | prospector = [ 879 | {file = "prospector-1.7.7-py3-none-any.whl", hash = "sha256:2dec5dac06f136880a3710996c0886dcc99e739007bbc05afc32884973f5c058"}, 880 | {file = "prospector-1.7.7.tar.gz", hash = "sha256:c04b3d593e7c525cf9a742fed62afbe02e2874f0e42f2f56a49378fd94037360"}, 881 | ] 882 | py = [ 883 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 884 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 885 | ] 886 | pycodestyle = [ 887 | {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, 888 | {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, 889 | ] 890 | pydocstyle = [ 891 | {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, 892 | {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, 893 | ] 894 | pyflakes = [] 895 | pylint = [] 896 | pylint-celery = [ 897 | {file = "pylint-celery-0.3.tar.gz", hash = "sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb"}, 898 | ] 899 | pylint-django = [ 900 | {file = "pylint-django-2.5.3.tar.gz", hash = "sha256:0ac090d106c62fe33782a1d01bda1610b761bb1c9bf5035ced9d5f23a13d8591"}, 901 | {file = "pylint_django-2.5.3-py3-none-any.whl", hash = "sha256:56b12b6adf56d548412445bd35483034394a1a94901c3f8571980a13882299d5"}, 902 | ] 903 | pylint-flask = [ 904 | {file = "pylint-flask-0.6.tar.gz", hash = "sha256:f4d97de2216bf7bfce07c9c08b166e978fe9f2725de2a50a9845a97de7e31517"}, 905 | ] 906 | pylint-plugin-utils = [ 907 | {file = "pylint-plugin-utils-0.7.tar.gz", hash = "sha256:ce48bc0516ae9415dd5c752c940dfe601b18fe0f48aa249f2386adfa95a004dd"}, 908 | {file = "pylint_plugin_utils-0.7-py3-none-any.whl", hash = "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535"}, 909 | ] 910 | pyparsing = [ 911 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 912 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 913 | ] 914 | pyrsistent = [ 915 | {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, 916 | {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"}, 917 | {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"}, 918 | {file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"}, 919 | {file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"}, 920 | {file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"}, 921 | {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"}, 922 | {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"}, 923 | {file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"}, 924 | {file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"}, 925 | {file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"}, 926 | {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"}, 927 | {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"}, 928 | {file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"}, 929 | {file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"}, 930 | {file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"}, 931 | {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"}, 932 | {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"}, 933 | {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"}, 934 | {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"}, 935 | {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, 936 | ] 937 | pytest = [ 938 | {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, 939 | {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, 940 | ] 941 | pytest-asyncio = [ 942 | {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, 943 | {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, 944 | ] 945 | pytest-cov = [ 946 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 947 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 948 | ] 949 | python-dateutil = [ 950 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 951 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 952 | ] 953 | pyyaml = [ 954 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 955 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 956 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 957 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 958 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 959 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 960 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 961 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 962 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 963 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 964 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 965 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 966 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 967 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 968 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 969 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 970 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 971 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 972 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 973 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 974 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 975 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 976 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 977 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 978 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 979 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 980 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 981 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 982 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 983 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 984 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 985 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 986 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 987 | ] 988 | questionary = [ 989 | {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, 990 | {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, 991 | ] 992 | requirements-detector = [ 993 | {file = "requirements-detector-0.7.tar.gz", hash = "sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"}, 994 | ] 995 | setoptconf-tmp = [ 996 | {file = "setoptconf-tmp-0.3.1.tar.gz", hash = "sha256:e0480addd11347ba52f762f3c4d8afa3e10ad0affbc53e3ffddc0ca5f27d5778"}, 997 | {file = "setoptconf_tmp-0.3.1-py3-none-any.whl", hash = "sha256:76035d5cd1593d38b9056ae12d460eca3aaa34ad05c315b69145e138ba80a745"}, 998 | ] 999 | six = [ 1000 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1001 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1002 | ] 1003 | smmap = [ 1004 | {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, 1005 | {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, 1006 | ] 1007 | snowballstemmer = [ 1008 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 1009 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 1010 | ] 1011 | stevedore = [] 1012 | toml = [ 1013 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1014 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1015 | ] 1016 | tomli = [ 1017 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1018 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1019 | ] 1020 | tomlkit = [ 1021 | {file = "tomlkit-0.11.4-py3-none-any.whl", hash = "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c"}, 1022 | {file = "tomlkit-0.11.4.tar.gz", hash = "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83"}, 1023 | ] 1024 | typer = [ 1025 | {file = "typer-0.6.1-py3-none-any.whl", hash = "sha256:54b19e5df18654070a82f8c2aa1da456a4ac16a2a83e6dcd9f170e291c56338e"}, 1026 | {file = "typer-0.6.1.tar.gz", hash = "sha256:2d5720a5e63f73eaf31edaa15f6ab87f35f0690f8ca233017d7d23d743a91d73"}, 1027 | ] 1028 | typing-extensions = [ 1029 | {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, 1030 | {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, 1031 | ] 1032 | wcwidth = [ 1033 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 1034 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 1035 | ] 1036 | websockets = [ 1037 | {file = "websockets-10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978"}, 1038 | {file = "websockets-10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500"}, 1039 | {file = "websockets-10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b"}, 1040 | {file = "websockets-10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c"}, 1041 | {file = "websockets-10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8"}, 1042 | {file = "websockets-10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677"}, 1043 | {file = "websockets-10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e"}, 1044 | {file = "websockets-10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f"}, 1045 | {file = "websockets-10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47"}, 1046 | {file = "websockets-10.3-cp310-cp310-win32.whl", hash = "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae"}, 1047 | {file = "websockets-10.3-cp310-cp310-win_amd64.whl", hash = "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079"}, 1048 | {file = "websockets-10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916"}, 1049 | {file = "websockets-10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb"}, 1050 | {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79"}, 1051 | {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d"}, 1052 | {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98"}, 1053 | {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e"}, 1054 | {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6"}, 1055 | {file = "websockets-10.3-cp37-cp37m-win32.whl", hash = "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1"}, 1056 | {file = "websockets-10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4"}, 1057 | {file = "websockets-10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36"}, 1058 | {file = "websockets-10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69"}, 1059 | {file = "websockets-10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd"}, 1060 | {file = "websockets-10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2"}, 1061 | {file = "websockets-10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c"}, 1062 | {file = "websockets-10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e"}, 1063 | {file = "websockets-10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991"}, 1064 | {file = "websockets-10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442"}, 1065 | {file = "websockets-10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76"}, 1066 | {file = "websockets-10.3-cp38-cp38-win32.whl", hash = "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559"}, 1067 | {file = "websockets-10.3-cp38-cp38-win_amd64.whl", hash = "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d"}, 1068 | {file = "websockets-10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094"}, 1069 | {file = "websockets-10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667"}, 1070 | {file = "websockets-10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731"}, 1071 | {file = "websockets-10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9"}, 1072 | {file = "websockets-10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680"}, 1073 | {file = "websockets-10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247"}, 1074 | {file = "websockets-10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af"}, 1075 | {file = "websockets-10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3"}, 1076 | {file = "websockets-10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8"}, 1077 | {file = "websockets-10.3-cp39-cp39-win32.whl", hash = "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582"}, 1078 | {file = "websockets-10.3-cp39-cp39-win_amd64.whl", hash = "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02"}, 1079 | {file = "websockets-10.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7"}, 1080 | {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f"}, 1081 | {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4"}, 1082 | {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755"}, 1083 | {file = "websockets-10.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55"}, 1084 | {file = "websockets-10.3.tar.gz", hash = "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"}, 1085 | ] 1086 | wrapt = [ 1087 | {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, 1088 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, 1089 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, 1090 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, 1091 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, 1092 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, 1093 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, 1094 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, 1095 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, 1096 | {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, 1097 | {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, 1098 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, 1099 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, 1100 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, 1101 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, 1102 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, 1103 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, 1104 | {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, 1105 | {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, 1106 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, 1107 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, 1108 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, 1109 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, 1110 | {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, 1111 | {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, 1112 | {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, 1113 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, 1114 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, 1115 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, 1116 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, 1117 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, 1118 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, 1119 | {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, 1120 | {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, 1121 | {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, 1122 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, 1123 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, 1124 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, 1125 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, 1126 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, 1127 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, 1128 | {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, 1129 | {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, 1130 | {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, 1131 | {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, 1132 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, 1133 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, 1134 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, 1135 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, 1136 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, 1137 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, 1138 | {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, 1139 | {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, 1140 | {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, 1141 | {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, 1142 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, 1143 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, 1144 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, 1145 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, 1146 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, 1147 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, 1148 | {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, 1149 | {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, 1150 | {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, 1151 | ] 1152 | --------------------------------------------------------------------------------