├── .circleci └── config.yml ├── .coveragerc ├── .github └── issue_template.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── appveyor.yml ├── ci ├── __init__.py ├── build.cmd ├── install.sh ├── mac-pre-install.sh └── tag.sh ├── docs ├── _ext │ ├── pulsarext.py │ ├── redisext.py │ └── sphinxtogithub.py ├── _static │ ├── actors.svg │ ├── favicon.ico │ ├── monitors.svg │ ├── pulsar-banner-600.svg │ ├── pulsar-banner.png │ ├── pulsar-banner.svg │ ├── pulsar-logo-linkedin.png │ ├── pulsar-logo.png │ ├── pulsar-logo.svg │ ├── pulsar.css │ ├── pulsar.png │ ├── pulsar3.png │ └── pulsar_logos │ │ ├── pulsar_colored_id_horizontal.svg │ │ ├── pulsar_colored_id_vertical.svg │ │ ├── pulsar_colored_logo_only.svg │ │ ├── pulsar_colored_text_only.svg │ │ ├── pulsar_monochome_id_horizontal.svg │ │ ├── pulsar_monochome_id_vertical.svg │ │ ├── pulsar_monochome_logo_only.svg │ │ └── pulsar_monochome_text_only.svg ├── _templates │ └── layout.html ├── acknowledgement.md ├── advantage.rst ├── api │ ├── actor.rst │ ├── application.rst │ ├── async.rst │ ├── clients.rst │ ├── events.rst │ ├── exceptions.rst │ ├── index.rst │ └── protocols.rst ├── apps │ ├── data │ │ ├── clients.rst │ │ ├── index.rst │ │ ├── pubsub.rst │ │ └── stores.rst │ ├── ds.rst │ ├── greenio.rst │ ├── http.rst │ ├── rpc.rst │ ├── socket.rst │ ├── test.rst │ ├── websockets.rst │ └── wsgi │ │ ├── async.rst │ │ ├── client.rst │ │ ├── content.rst │ │ ├── index.rst │ │ ├── middleware.rst │ │ ├── response.rst │ │ ├── routing.rst │ │ ├── server.rst │ │ ├── tools.rst │ │ └── wrappers.rst ├── artwork │ ├── actor.sketch │ └── pulsar.sketch ├── changelog.md ├── conf.py ├── design.rst ├── faq.rst ├── history │ ├── 1.0.md │ ├── 1.1.md │ ├── 1.2.md │ ├── 1.3.md │ ├── 1.4.md │ ├── 1.5.md │ ├── 1.6.md │ └── pre1.0.rst ├── index.rst ├── internals │ ├── actor.rst │ ├── index.rst │ ├── mixins.rst │ └── utilities.rst ├── overview.rst ├── pics.rst ├── settings.rst ├── spelling_wordlist.txt └── tutorials │ ├── actors.rst │ ├── benchmarking.rst │ ├── calculator.rst │ ├── cextensions.rst │ ├── chat.rst │ ├── clients.rst │ ├── command.rst │ ├── deploy.rst │ ├── events.rst │ ├── flask.rst │ ├── hello.rst │ ├── httpbin.rst │ ├── index.rst │ ├── logging.rst │ ├── messages.rst │ ├── philosophers.rst │ ├── proxy.rst │ ├── pulsards.rst │ ├── signal.rst │ ├── sync.rst │ ├── udp.rst │ └── wsgi.rst ├── examples ├── __init__.py ├── calculator │ ├── __init__.py │ ├── manage.py │ └── tests.py ├── chat │ ├── __init__.py │ ├── chat.html │ ├── manage.py │ ├── media │ │ └── chat.js │ └── tests.py ├── echo │ ├── __init__.py │ ├── manage.py │ └── tests.py ├── echoudp │ ├── __init__.py │ ├── manage.py │ └── tests.py ├── flaskapp │ ├── __init__.py │ ├── manage.py │ └── tests.py ├── helloworld │ ├── __init__.py │ ├── manage.py │ └── tests.py ├── httpbin │ ├── __init__.py │ ├── assets │ │ ├── favicon.ico │ │ ├── httpbin.css │ │ ├── httpbin.js │ │ ├── stats.html │ │ ├── template.html │ │ └── websocket.html │ ├── config.py │ ├── httpbin.conf │ ├── httpbin.jmx │ ├── manage.py │ ├── server.crt │ └── server.key ├── philosophers │ ├── __init__.py │ ├── manage.py │ └── tests.py ├── proxyserver │ ├── __init__.py │ └── manage.py ├── pulsarapp │ ├── __init__.py │ ├── manage.py │ └── tests.py ├── pulsards │ ├── __init__.py │ └── manage.py ├── snippets │ ├── __init__.py │ ├── actor1.py │ ├── greeter.py │ ├── hello.py │ ├── remote.py │ └── tunnel.py └── websocket │ ├── __init__.py │ ├── manage.py │ ├── tests.py │ └── websocket.html ├── extensions ├── __init__.py ├── ext.py └── lib │ ├── clib.pxd │ ├── clib.pyx │ ├── events.pyx │ ├── globals.pyx │ ├── http.h │ ├── protocols.pyx │ ├── rparser.pyx │ ├── utils.pyx │ ├── websocket.h │ ├── websocket.pxd │ ├── websocket.pyx │ ├── wsgi.pxd │ ├── wsgi.pyx │ └── wsgiresponse.pyx ├── install-dev.sh ├── pulsar ├── __init__.py ├── api.py ├── apps │ ├── __init__.py │ ├── data │ │ ├── __init__.py │ │ ├── channels.py │ │ ├── pulsards │ │ │ ├── __init__.py │ │ │ └── startds.py │ │ ├── redis │ │ │ ├── __init__.py │ │ │ ├── client.py │ │ │ ├── lock.py │ │ │ ├── pubsub.py │ │ │ └── store.py │ │ └── store.py │ ├── ds │ │ ├── __init__.py │ │ ├── client.py │ │ ├── parser.py │ │ ├── server.py │ │ └── utils.py │ ├── greenio │ │ ├── __init__.py │ │ ├── http.py │ │ ├── lock.py │ │ ├── pool.py │ │ ├── utils.py │ │ └── wsgi.py │ ├── http │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── client.py │ │ ├── decompress.py │ │ ├── oauth.py │ │ ├── plugins.py │ │ ├── stream.py │ │ └── wsgi.py │ ├── rpc │ │ ├── __init__.py │ │ ├── handlers.py │ │ ├── jsonrpc.py │ │ └── mixins.py │ ├── socket │ │ └── __init__.py │ ├── test │ │ ├── __init__.py │ │ ├── cov.py │ │ ├── loader.py │ │ ├── plugins │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── bench.py │ │ │ ├── htmlfiles │ │ │ │ └── profile │ │ │ │ │ ├── index.html │ │ │ │ │ ├── jquery.tablesorter.min.js │ │ │ │ │ ├── profile.js │ │ │ │ │ └── style.css │ │ │ └── profile.py │ │ ├── populate.py │ │ ├── result.py │ │ ├── runner.py │ │ ├── utils.py │ │ └── wsgi.py │ ├── ws │ │ ├── __init__.py │ │ ├── extensions.py │ │ └── websocket.py │ └── wsgi │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── content.py │ │ ├── formdata.py │ │ ├── handlers.py │ │ ├── headers.py │ │ ├── html.py │ │ ├── middleware.py │ │ ├── response.py │ │ ├── route.py │ │ ├── routers.py │ │ ├── server.py │ │ ├── structures.py │ │ ├── utils.py │ │ └── wrappers.py ├── async │ ├── __init__.py │ ├── _subprocess.py │ ├── access.py │ ├── actor.py │ ├── clients.py │ ├── commands.py │ ├── concurrency.py │ ├── consts.py │ ├── cov.py │ ├── futures.py │ ├── lock.py │ ├── mailbox.py │ ├── mixins.py │ ├── monitor.py │ ├── process.py │ ├── protocols.py │ ├── proxy.py │ ├── threads.py │ └── timeout.py ├── cmds │ ├── __init__.py │ ├── build-wheels.sh │ ├── linux_wheels.py │ ├── pypi_version.py │ ├── s3data.py │ └── test.py └── utils │ ├── __init__.py │ ├── autoreload.py │ ├── config.py │ ├── context.py │ ├── exceptions.py │ ├── html.py │ ├── http │ ├── __init__.py │ └── parser.py │ ├── httpurl.py │ ├── importer.py │ ├── internet.py │ ├── lib.py │ ├── log.py │ ├── path.py │ ├── profiler.py │ ├── pylib │ ├── __init__.py │ ├── events.py │ ├── protocols.py │ ├── redisparser.py │ ├── websocket.py │ ├── wsgi.py │ └── wsgiresponse.py │ ├── security.py │ ├── slugify.py │ ├── string.py │ ├── structures │ ├── __init__.py │ ├── misc.py │ ├── skiplist.py │ └── zset.py │ ├── system │ ├── __init__.py │ ├── base.py │ ├── posixsystem.py │ ├── runtime.py │ ├── windowssystem.py │ ├── winprocess.py │ └── winservice.py │ ├── tools │ ├── __init__.py │ ├── arity.py │ ├── numbers.py │ ├── pidfile.py │ └── text.py │ ├── version.py │ ├── websocket.py │ └── wsgi_py.py ├── requirements ├── ci.txt ├── dev.txt ├── docs.txt ├── hard.txt ├── test-docs.txt ├── test-posix.txt ├── test-win.txt └── test.txt ├── runtests.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── apps ├── __init__.py ├── test_greenio.py ├── test_multi.py └── test_rpc.py ├── async ├── __init__.py ├── actor.py ├── test_actor_coroutine.py ├── test_actor_process.py ├── test_actor_subprocess.py ├── test_actor_thread.py ├── test_api.py ├── test_arbiter.py ├── test_events.py └── test_lock.py ├── bench ├── test_coroutine.py ├── test_events.py ├── test_green.py ├── test_http_date.py ├── test_python.py ├── test_redis.py ├── test_websocket.py └── test_wsgi.py ├── http ├── __init__.py ├── base.py ├── ca_bundle ├── req.py ├── test_bug.py ├── test_client.py ├── test_corners.py ├── test_parser.py ├── test_proxy.py ├── test_tls.py ├── test_tools.py └── test_tunnel.py ├── stores ├── __init__.py ├── channels.py ├── lock.py ├── test_parser.py ├── test_pulsards.py ├── test_redis.py └── test_utils.py ├── suite ├── __init__.py ├── test_coverage.py ├── test_failures.py ├── test_loader.py └── test_me.py ├── utils ├── __init__.py ├── structures │ ├── test_skiplist.py │ ├── test_structures.py │ └── test_zset.py ├── test_autoreload.py ├── test_config.py ├── test_context.py ├── test_ext.py ├── test_frame.py ├── test_git.py ├── test_internet.py ├── test_misc.py ├── test_mixins.py ├── test_path.py ├── test_pid.py ├── test_slugify.py ├── test_string.py ├── test_system.py ├── test_text.py └── test_tools.py └── wsgi ├── ab.txt ├── test_accept.py ├── test_content.py ├── test_errors.py ├── test_html.py ├── test_route.py ├── test_router.py └── test_wsgi.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = pulsar,examples 3 | concurrency = greenlet,multiprocessing 4 | omit = 5 | pulsar/__init__.py 6 | pulsar/utils/system/winservice.py 7 | pulsar/utils/system/windowssystem.py 8 | pulsar/utils/system/winprocess.py 9 | pulsar/utils/autoreload.py 10 | pulsar/utils/version.py 11 | pulsar/async/_subprocess.py 12 | pulsar/async/cov.py 13 | pulsar/apps/test/plugins/bench.py 14 | pulsar/apps/test/cov.py 15 | pulsar/apps/test/plugins/profile.py 16 | pulsar/cmds/* 17 | examples/httpbin/config.py 18 | examples/httpbin/throttle.py 19 | examples/philosophers/config.py 20 | examples/snippets/* 21 | examples/pulsards/* 22 | examples/tweets/* 23 | 24 | [report] 25 | # Regexes for lines to exclude from consideration 26 | exclude_lines = 27 | (?i)# *pragma[: ]*no *cover 28 | raise NotImplementedError 29 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | * **pulsar version**: 2 | * **python version**: 3 | * **platform**: 4 | 5 | ## Description 6 | 7 | 8 | 9 | ## Expected behaviour 10 | 11 | 12 | 13 | ## Actual behaviour 14 | 15 | 16 | 17 | ## Steps to reproduce 18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyd 3 | *.so 4 | *.o 5 | *.def 6 | 7 | # Python 8 | __pycache__ 9 | build 10 | docs/build 11 | dist 12 | wheelhouse 13 | htmlcov 14 | htmlprof 15 | venv 16 | .coverage 17 | .coveralls-repo-token 18 | MANIFEST 19 | .settings 20 | .python-version 21 | release-notes.md 22 | extensions/lib/clib.c 23 | 24 | # IDE 25 | .DS_Store 26 | .project 27 | .pydevproject 28 | .idea 29 | 30 | # Extensions 31 | *~ 32 | .htmlprof 33 | test.pid 34 | *.egg-info 35 | .eggs 36 | .coveralls.yml 37 | *.sqlite3 38 | *.rdb 39 | *.log 40 | *.mp4 41 | /config.py 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | osx_image: xcode9.1 4 | 5 | branches: 6 | only: 7 | - release 8 | 9 | env: 10 | global: 11 | - PYMODULE=pulsar 12 | - WHEEL=macosx 13 | 14 | matrix: 15 | fast_finish: true 16 | 17 | include: 18 | - os: osx 19 | env: PYTHON_VERSION=3.5.4 20 | 21 | - os: osx 22 | env: PYTHON_VERSION=3.6.3 23 | 24 | install: 25 | - ci/mac-pre-install.sh 26 | - source venv/bin/activate && ci/install.sh 27 | 28 | script: 29 | - source venv/bin/activate && make pypi-check 30 | - source venv/bin/activate && make wheels-mac 31 | - source venv/bin/activate && make wheels-upload 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2017 Quantmind 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation 10 | and/or other materials provided with the distribution. 11 | * Neither the name of the author nor the names of its contributors 12 | may be used to endorse or promote products derived from this software without 13 | specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include Makefile 4 | graft pulsar 5 | graft docs 6 | graft requirements 7 | graft examples 8 | graft extensions 9 | graft tests 10 | global-exclude *.pyc 11 | global-exclude *.pyd 12 | global-exclude *.so 13 | global-exclude *.mp4 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | PYTHON ?= python 3 | PIP ?= pip 4 | DOCS_SOURCE ?= docs 5 | DOCS_BUILDDIR ?= build/docs 6 | 7 | .PHONY: help 8 | 9 | help: 10 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' 11 | 12 | clean: ## clean build directory and cache files 13 | rm -fr dist/ *.eggs .eggs build/ pulsar/utils/*.so extensions/lib/clib.c 14 | find . -name '__pycache__' | xargs rm -rf 15 | find . -name '*.pyc' | xargs rm -rf 16 | find . -name '.DS_Store' | xargs rm -rf 17 | 18 | compile: ## clean and build extension in place (for development) 19 | make clean 20 | $(PYTHON) setup.py build_ext -i 21 | 22 | docs: ## build html documentation 23 | mkdir -p build/docs/html 24 | $(PYTHON) -m sphinx -a -b html $(DOCS_SOURCE) $(DOCS_BUILDDIR)/html 25 | 26 | docs-spelling: ## check documentation spelling 27 | $(PYTHON) -m sphinx -a -b spelling $(DOCS_SOURCE) $(DOCS_BUILDDIR)/spelling 28 | 29 | test: ## flake8 and unit-tests with uvloop 30 | flake8 31 | $(PYTHON) -W ignore setup.py test -q --io uv 32 | 33 | testpy: ## pure python library unit tests (PULSARPY=yes) 34 | export PULSARPY=yes 35 | $(PYTHON) -W ignore setup.py test -q 36 | 37 | coverage: ## run tunit tests with coverage 38 | export PULSARPY=yes; $(PYTHON) -W ignore setup.py test --coverage -q 39 | 40 | testall: 41 | flake8 42 | $(PYTHON) -W ignore setup.py test -q 43 | $(PYTHON) -W ignore setup.py test -q --io uv 44 | $(PYTHON) setup.py bench 45 | 46 | pypi-check: ## check if current version is valid for a new pypi release 47 | $(PYTHON) setup.py pypi --final 48 | 49 | wheels: ## build platform wheels 50 | make clean 51 | $(PYTHON) setup.py bdist_wheel 52 | 53 | wheels-mac: ## create wheels for Mac OSX **must be run from a mac** 54 | export PYMODULE=pulsar; export WHEEL=macosx; export CI=true; ./pulsar/cmds/build-wheels.sh 55 | 56 | wheels-linux: ## create linux wheels for python 3.5 & 3.6 57 | rm -rf wheelhouse 58 | $(PYTHON) setup.py linux_wheels --py 3.5,3.6 59 | 60 | wheels-test: ## run tests using wheels distribution 61 | rm -rf tmp 62 | mkdir tmp 63 | cp -r tests tmp/tests 64 | cp -r examples tmp/examples 65 | cp -r docs tmp/docs 66 | cp runtests.py tmp/runtests.py 67 | cd tmp && $(PYTHON) runtests.py 68 | rm -rf tmp 69 | 70 | wheels-upload: ## upload wheels to s3 71 | $(PYTHON) setup.py s3data --bucket fluidily --key wheelhouse --files "wheelhouse/*.whl" 72 | 73 | wheels-download:## download wheels from s3 74 | $(PYTHON) setup.py s3data --bucket fluidily --key wheelhouse --download 75 | 76 | release: clean compile test 77 | $(PYTHON) setup.py sdist bdist_wheel upload 78 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | PYPI_PASSWD: 3 | secure: w16EmxgfwQdS1FLB/PCWQA== 4 | matrix: 5 | - PYTHON: "C:\\Python35" 6 | - PYTHON: "C:\\Python35-x64" 7 | - PYTHON: "C:\\Python36" 8 | - PYTHON: "C:\\Python36-x64" 9 | 10 | branches: 11 | only: 12 | - release 13 | - deploy 14 | 15 | init: 16 | - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" 17 | 18 | install: 19 | - "ci/build.cmd %PYTHON%\\python.exe -m pip install -U wheel setuptools" 20 | - "ci/build.cmd %PYTHON%\\python.exe -m pip install -r requirements/test-win.txt" 21 | - cmd: nuget install redis-64 -excludeversion 22 | - cmd: redis-64\tools\redis-server.exe --service-install 23 | - cmd: redis-64\tools\redis-server.exe --service-start 24 | 25 | build: false 26 | 27 | test_script: 28 | - ps: >- 29 | if($env:appveyor_repo_branch -eq 'release') { 30 | Invoke-Expression "ci/build.cmd $env:PYTHON\\python.exe setup.py test --sequential" 31 | } 32 | 33 | after_test: 34 | - "ci/build.cmd %PYTHON%\\python.exe setup.py sdist bdist_wheel" 35 | 36 | artifacts: 37 | - path: dist\* 38 | 39 | deploy_script: 40 | - ps: >- 41 | if($env:appveyor_repo_branch -eq 'deploy') { 42 | Invoke-Expression "$env:PYTHON\\python.exe -m twine upload dist/* --username lsbardel --password $env:PYPI_PASSWD" 43 | } 44 | -------------------------------------------------------------------------------- /ci/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/ci/__init__.py -------------------------------------------------------------------------------- /ci/build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: To build extensions for 64 bit Python 3, we need to configure environment 3 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 4 | :: MS Windows SDK for Windows 7 and .NET Framework 4 5 | :: 6 | :: More details at: 7 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 8 | 9 | IF "%DISTUTILS_USE_SDK%"=="1" ( 10 | ECHO Configuring environment to build with MSVC on a 64bit architecture 11 | ECHO Using Windows SDK 7.1 12 | "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1 13 | CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release 14 | SET MSSdk=1 15 | REM Need the following to allow tox to see the SDK compiler 16 | SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB 17 | ) ELSE ( 18 | ECHO Using default MSVC build environment 19 | ) 20 | 21 | CALL %* 22 | -------------------------------------------------------------------------------- /ci/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pip install --upgrade pip wheel 4 | pip install --upgrade setuptools 5 | pip install -r requirements/ci.txt 6 | pip install -r requirements/test-posix.txt 7 | pip install -r requirements/hard.txt 8 | -------------------------------------------------------------------------------- /ci/mac-pre-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -x 4 | 5 | brew update 6 | 7 | brew upgrade pyenv 8 | brew install gnu-sed --with-default-names 9 | brew outdated libtool || brew upgrade libtool 10 | brew outdated autoconf || brew upgrade autoconf --with-default-names 11 | brew outdated automake || brew upgrade automake --with-default-names 12 | 13 | 14 | if ! (pyenv versions | grep "${PYTHON_VERSION}$"); then 15 | pyenv install ${PYTHON_VERSION} 16 | fi 17 | pyenv global ${PYTHON_VERSION} 18 | pyenv rehash 19 | 20 | pyenv exec pip install virtualenv 21 | pyenv exec virtualenv venv 22 | -------------------------------------------------------------------------------- /ci/tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION="$(python setup.py --version)" 4 | echo ${VERSION} 5 | 6 | git config --global user.email "bot@quantmind.com" 7 | git config --global user.username "qmbot" 8 | git config --global user.name "Quantmind Bot" 9 | git push 10 | git tag -am "Release $VERSION [ci skip]" ${VERSION} 11 | git push --tags 12 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/pulsar-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/docs/_static/pulsar-banner.png -------------------------------------------------------------------------------- /docs/_static/pulsar-logo-linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/docs/_static/pulsar-logo-linkedin.png -------------------------------------------------------------------------------- /docs/_static/pulsar-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/docs/_static/pulsar-logo.png -------------------------------------------------------------------------------- /docs/_static/pulsar.css: -------------------------------------------------------------------------------- 1 | 2 | a.reference.image-reference { 3 | border-bottom: none; 4 | } 5 | -------------------------------------------------------------------------------- /docs/_static/pulsar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/docs/_static/pulsar.png -------------------------------------------------------------------------------- /docs/_static/pulsar3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/docs/_static/pulsar3.png -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {# layout.html #} 2 | {# Import the theme's layout. #} 3 | {% extends "!layout.html" %} 4 | 5 | {% set css_files = css_files + ['_static/pulsar.css'] %} 6 | -------------------------------------------------------------------------------- /docs/acknowledgement.md: -------------------------------------------------------------------------------- 1 | # Acknowledgement 2 | 3 | This is a list of modules and snipped of codes taken from the wonderful 4 | python open-source community. In order of inclusion into pulsar: 5 | 6 | * ``parse_header_links`` from [requests][] 7 | ``` 8 | Copyright 2016 Kenneth Reitz 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License") 11 | ``` 12 | 13 | 14 | 15 | [requests]: http://docs.python-requests.org/en/master/ 16 | -------------------------------------------------------------------------------- /docs/advantage.rst: -------------------------------------------------------------------------------- 1 | .. _pulsar-advantage: 2 | 3 | Pulsar Advantage 4 | ======================== 5 | 6 | 7 | Python 3 - Asyncio 8 | -------------------- 9 | Pulsar is written in python 3 and it is built on top of asyncio_, the new 10 | asynchronous module in the python standard library. 11 | 12 | Flexibility 13 | ------------------- 14 | Pulsar's codebase is relatively small, at about 20,000 lines of code. 15 | The source code is open and it has a very :ref:`liberal license `. 16 | You can :ref:`fork the code ` add feature you need and send 17 | us your patch. 18 | 19 | Multiprocessing 20 | ------------------- 21 | Multiprocessing is the default parallel execution mechanism, therefore 22 | each pulsar components have been designed in a share nothing architecture. 23 | Communication between workers is obtained via tcp sockets which 24 | :ref:`exchange messages ` using the websocket protocol. 25 | You can also run workers in threading mode. 26 | 27 | Documented and Samples 28 | --------------------------- 29 | Pulsar documentation is continuously updated and extended and there are several 30 | examples in the :mod:`examples` module of the distribution directory. 31 | 32 | 33 | .. _asyncio: http://python.readthedocs.org/en/latest/library/asyncio.html 34 | -------------------------------------------------------------------------------- /docs/api/actor.rst: -------------------------------------------------------------------------------- 1 | .. module:: pulsar.api 2 | 3 | .. _actor-api: 4 | 5 | ======================= 6 | Actors API 7 | ======================= 8 | 9 | For an overview of pulsar actors 10 | check out the :ref:`design documentation `. 11 | 12 | 13 | .. _spawn-function: 14 | 15 | spawn 16 | ============ 17 | 18 | .. autofunction:: spawn 19 | 20 | .. _send-function: 21 | 22 | send 23 | ============ 24 | 25 | .. autofunction:: send 26 | 27 | 28 | get_actor 29 | ============ 30 | 31 | .. function:: get_actor 32 | 33 | Returns the :class:`Actor` controlling the current thread. 34 | Returns ``None`` if no actor is available. 35 | 36 | arbiter 37 | ============ 38 | 39 | .. autofunction:: arbiter 40 | 41 | 42 | command 43 | ~~~~~~~~~~~~~~~~~~~~ 44 | 45 | .. autoclass:: command 46 | 47 | -------------------------------------------------------------------------------- /docs/api/application.rst: -------------------------------------------------------------------------------- 1 | .. _application-api: 2 | 3 | ======================== 4 | Application API 5 | ======================== 6 | 7 | .. automodule:: pulsar.apps 8 | -------------------------------------------------------------------------------- /docs/api/async.rst: -------------------------------------------------------------------------------- 1 | .. _async-api: 2 | 3 | .. module:: pulsar.api 4 | 5 | ================== 6 | Asynchronous API 7 | ================== 8 | 9 | Pulsar asynchronous api is built on top of the new python :mod:`asyncio` 10 | module. 11 | 12 | 13 | Async object 14 | ============== 15 | 16 | .. autoclass:: AsyncObject 17 | :members: 18 | :member-order: bysource 19 | 20 | .. _async-discovery: 21 | 22 | 23 | Chain Future 24 | ================ 25 | .. autofunction:: chain_future 26 | 27 | 28 | Async While 29 | =============== 30 | .. autofunction:: async_while 31 | 32 | 33 | 34 | Base Lock 35 | ========== 36 | 37 | .. autoclass:: LockBase 38 | :members: 39 | :member-order: bysource 40 | 41 | 42 | Lock 43 | ========== 44 | 45 | 46 | .. autoclass:: Lock 47 | :members: 48 | :member-order: bysource 49 | -------------------------------------------------------------------------------- /docs/api/clients.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | .. module:: pulsar.api 5 | 6 | .. _clients-api: 7 | 8 | ===================== 9 | Clients 10 | ================= 11 | 12 | 13 | This section introduces classes implementing the transport/protocol paradigm 14 | for clients with several connections to a remote :class:`.TcpServer`. 15 | 16 | 17 | Abstract Client 18 | ===================== 19 | 20 | .. autoclass:: AbstractClient 21 | :members: 22 | :member-order: bysource 23 | 24 | 25 | Pool 26 | ===================== 27 | 28 | .. autoclass:: Pool 29 | :members: 30 | :member-order: bysource 31 | 32 | 33 | Pool Connection 34 | ===================== 35 | 36 | .. autoclass:: PoolConnection 37 | :members: 38 | :member-order: bysource 39 | 40 | -------------------------------------------------------------------------------- /docs/api/events.rst: -------------------------------------------------------------------------------- 1 | .. module:: pulsar.api 2 | 3 | ============ 4 | Event API 5 | ============ 6 | 7 | The :class:`.EventHandler` class is for creating objects with events. 8 | These events can occur once only during the life of an :class:`.EventHandler` 9 | or can occur several times. Check the 10 | :ref:`event dispatching tutorial ` for an overview. 11 | 12 | 13 | Events 14 | ================== 15 | 16 | .. autoclass:: Event 17 | :members: 18 | :member-order: bysource 19 | 20 | 21 | Events Handler 22 | ================== 23 | 24 | .. autoclass:: EventHandler 25 | :members: 26 | :member-order: bysource 27 | -------------------------------------------------------------------------------- /docs/api/exceptions.rst: -------------------------------------------------------------------------------- 1 | .. _exceptions: 2 | 3 | ================== 4 | Exceptions 5 | ================== 6 | 7 | .. automodule:: pulsar.utils.exceptions 8 | :members: 9 | :member-order: bysource -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | ================== 4 | API 5 | ================== 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | async 11 | actor 12 | events 13 | protocols 14 | clients 15 | application 16 | exceptions 17 | -------------------------------------------------------------------------------- /docs/api/protocols.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _protocol-api: 3 | 4 | ================================ 5 | Protocols/Transports API 6 | ================================ 7 | 8 | This part of the :ref:`pulsar API ` is about classes responsible for 9 | implementing the Protocol/Transport paradigm. They are based on 10 | :class:`asyncio.Protocol` and :class:`asyncio.DatagramProtocol` classes. 11 | 12 | 13 | .. module:: pulsar.api 14 | 15 | 16 | 17 | PulsarProtocol 18 | ================== 19 | .. autoclass:: PulsarProtocol 20 | :members: 21 | :member-order: bysource 22 | 23 | 24 | Producer 25 | ================== 26 | .. autoclass:: Producer 27 | :members: 28 | :member-order: bysource 29 | 30 | 31 | Protocol 32 | ================== 33 | .. autoclass:: Protocol 34 | :members: 35 | :member-order: bysource 36 | 37 | Connection 38 | ================== 39 | .. autoclass:: Connection 40 | :members: 41 | :member-order: bysource 42 | 43 | Protocol Consumer 44 | ================== 45 | .. autoclass:: ProtocolConsumer 46 | :members: 47 | :member-order: bysource 48 | 49 | 50 | Producers 51 | ================= 52 | 53 | Producers are factory of :class:`.Protocol` with end-points. 54 | They are used by both servers and clients classes. 55 | 56 | Producer 57 | ~~~~~~~~~~~~~~~~~ 58 | .. autoclass:: Producer 59 | :members: 60 | :member-order: bysource 61 | 62 | 63 | TCP Server 64 | ~~~~~~~~~~~~~~~~~ 65 | 66 | .. autoclass:: TcpServer 67 | :members: 68 | :member-order: bysource 69 | 70 | 71 | UDP 72 | ===== 73 | 74 | Classes for the (user) datagram protocol. UDP uses a simple transmission 75 | model with a minimum of protocol mechanism. 76 | 77 | 78 | Datagram Protocol 79 | ~~~~~~~~~~~~~~~~~~ 80 | .. autoclass:: DatagramProtocol 81 | :members: 82 | :member-order: bysource 83 | 84 | Datagram Server 85 | ~~~~~~~~~~~~~~~~~~ 86 | .. autoclass:: DatagramServer 87 | :members: 88 | :member-order: bysource 89 | 90 | 91 | .. _pep-3153: http://www.python.org/dev/peps/pep-3153/ 92 | .. _pep-3156: http://www.python.org/dev/peps/pep-3156/ 93 | -------------------------------------------------------------------------------- /docs/apps/data/index.rst: -------------------------------------------------------------------------------- 1 | .. _apps-data: 2 | 3 | ================= 4 | Data Stores 5 | ================= 6 | 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | clients 12 | stores 13 | pubsub 14 | -------------------------------------------------------------------------------- /docs/apps/data/pubsub.rst: -------------------------------------------------------------------------------- 1 | .. _apps-pubsub: 2 | 3 | Publish/Subscribe 4 | ===================== 5 | 6 | A data :class:`.Store` can implement the :meth:`~.RemoteStore.pubsub` method to return 7 | a valid :class:`.PubSub` handler. 8 | 9 | 10 | Channels 11 | ----------- 12 | 13 | Channels are an high level object which uses a :class:`.PubSub` handler to manage 14 | events on channels. They are useful for: 15 | 16 | * Reducing the number of channels to subscribe to by introducing channel events 17 | * Manage registration and un-registration to channel events 18 | * Handle reconnection with exponential back-off 19 | 20 | API 21 | ---- 22 | 23 | PubSub 24 | ~~~~~~~~~~~~~~~ 25 | 26 | .. autoclass:: pulsar.apps.data.PubSub 27 | :members: 28 | :member-order: bysource 29 | 30 | 31 | Channels 32 | ~~~~~~~~~~~~~~~ 33 | 34 | .. autoclass:: pulsar.apps.data.Channels 35 | :members: 36 | :member-order: bysource 37 | -------------------------------------------------------------------------------- /docs/apps/data/stores.rst: -------------------------------------------------------------------------------- 1 | .. _builtin-data-stores: 2 | 3 | ==================== 4 | Available Clients 5 | ==================== 6 | 7 | .. _store_redis: 8 | 9 | Redis 10 | ================================ 11 | 12 | .. automodule:: pulsar.apps.data.redis 13 | 14 | 15 | .. _store_pulsar: 16 | 17 | PulsarDs 18 | ================================ 19 | 20 | It has the same implementation as :ref:`redis client `. 21 | -------------------------------------------------------------------------------- /docs/apps/ds.rst: -------------------------------------------------------------------------------- 1 | .. _pulsar-data-store: 2 | 3 | .. module:: pulsar.apps.ds 4 | 5 | =============================== 6 | Pulsar Data Store Server 7 | =============================== 8 | 9 | .. automodule:: pulsar.apps.ds.server 10 | 11 | 12 | Commands 13 | =================== 14 | 15 | .. redis_commands:: 16 | -------------------------------------------------------------------------------- /docs/apps/rpc.rst: -------------------------------------------------------------------------------- 1 | .. _apps-rpc: 2 | 3 | ============================ 4 | JSON-RPC 5 | ============================ 6 | 7 | .. automodule:: pulsar.apps.rpc -------------------------------------------------------------------------------- /docs/apps/socket.rst: -------------------------------------------------------------------------------- 1 | .. _apps-socket: 2 | 3 | ================= 4 | Socket Servers 5 | ================= 6 | 7 | .. automodule:: pulsar.apps.socket 8 | 9 | 10 | API 11 | ========= 12 | 13 | Socket Server 14 | ~~~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | .. autoclass:: SocketServer 17 | :members: 18 | :member-order: bysource 19 | 20 | 21 | UDP Socket Server 22 | ~~~~~~~~~~~~~~~~~~~~~~~ 23 | 24 | .. autoclass:: UdpSocketServer 25 | :members: 26 | :member-order: bysource 27 | -------------------------------------------------------------------------------- /docs/apps/websockets.rst: -------------------------------------------------------------------------------- 1 | .. _apps-ws: 2 | 3 | ============= 4 | WebSockets 5 | ============= 6 | 7 | .. automodule:: pulsar.apps.ws 8 | -------------------------------------------------------------------------------- /docs/apps/wsgi/client.rst: -------------------------------------------------------------------------------- 1 | .. _apps-wsgi-client: 2 | 3 | =============================== 4 | Wsgi Client 5 | =============================== 6 | 7 | The :class:`.HttpWsgiClient` is a tool for interacting with wsgi callable 8 | using the same API as the :class:`.HttpClient`. 9 | -------------------------------------------------------------------------------- /docs/apps/wsgi/index.rst: -------------------------------------------------------------------------------- 1 | .. _apps-wsgi: 2 | 3 | ================= 4 | WSGI 5 | ================= 6 | 7 | The :mod:`~.apps.wsgi` module implements a :ref:`web server ` 8 | and several web :ref:`application handlers ` which 9 | conform with pulsar :ref:`WSGI asynchronous specification `. 10 | In addition, the module contains several utilities which facilitate the 11 | development of server side asynchronous web applications. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | async 17 | server 18 | routing 19 | wrappers 20 | middleware 21 | response 22 | content 23 | tools 24 | client 25 | -------------------------------------------------------------------------------- /docs/apps/wsgi/middleware.rst: -------------------------------------------------------------------------------- 1 | .. _wsgi-middleware: 2 | 3 | =============================== 4 | Asynchronous Middleware 5 | =============================== 6 | 7 | .. automodule:: pulsar.apps.wsgi.middleware 8 | -------------------------------------------------------------------------------- /docs/apps/wsgi/response.rst: -------------------------------------------------------------------------------- 1 | .. _response-middleware: 2 | 3 | =============================== 4 | Response Middleware 5 | =============================== 6 | 7 | .. automodule:: pulsar.apps.wsgi.response 8 | -------------------------------------------------------------------------------- /docs/apps/wsgi/server.rst: -------------------------------------------------------------------------------- 1 | .. _wsgi-server: 2 | 3 | ================= 4 | WSGI Server 5 | ================= 6 | 7 | .. automodule:: pulsar.apps.wsgi 8 | 9 | .. automodule:: pulsar.apps.wsgi.server 10 | 11 | -------------------------------------------------------------------------------- /docs/apps/wsgi/tools.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Utilities 3 | ===================== 4 | 5 | 6 | Authentication 7 | ================= 8 | 9 | .. automodule:: pulsar.apps.wsgi.auth 10 | 11 | 12 | Structures 13 | ================= 14 | 15 | .. automodule:: pulsar.apps.wsgi.structures 16 | :members: 17 | 18 | Miscellaneous 19 | ================ 20 | 21 | .. automodule:: pulsar.apps.wsgi.utils 22 | :members: 23 | 24 | 25 | .. _http-parser: https://github.com/benoitc/http-parser 26 | .. _cython: http://cython.org/ 27 | -------------------------------------------------------------------------------- /docs/apps/wsgi/wrappers.rst: -------------------------------------------------------------------------------- 1 | .. _apps-wsgi-wrappers: 2 | 3 | =============================== 4 | WSGI Wrappers 5 | =============================== 6 | 7 | .. automodule:: pulsar.apps.wsgi.wrappers -------------------------------------------------------------------------------- /docs/artwork/actor.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/docs/artwork/actor.sketch -------------------------------------------------------------------------------- /docs/artwork/pulsar.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/docs/artwork/pulsar.sketch -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | * [Version 1.6](./history/1.6.html) 4 | * [Version 1.5](./history/1.5.html) 5 | * [Version 1.4](./history/1.4.html) 6 | * [Version 1.3](./history/1.3.html) 7 | * [Version 1.2](./history/1.2.html) 8 | * [Version 1.1](./history/1.1.html) 9 | * [Version 1.0](./history/1.0.html) 10 | * [Pre Version 1.0](./history/pre1.0.html) 11 | -------------------------------------------------------------------------------- /docs/history/1.2.md: -------------------------------------------------------------------------------- 1 | ## Ver. 1.2.0 - 2016-Mar-17 2 | 3 | Pulsar 1.2 is broadly backward compatible with the [1.1 version](./1.1.html) 4 | with the only caveat that the ``is_async`` function is now deprecated and replaced 5 | by the ``isawaitable`` function. 6 | Warnings will be issued when used by client applications. 7 | 8 | In addition, pulsar **1.2.x** is the last minor release ("major.minor.micro") to support python 3.4. 9 | From pulsar **1.3.x**, support for python 3.4 will be dropped and the new 10 | [async-await](https://www.python.org/dev/peps/pep-0492/#new-coroutine-declaration-syntax) 11 | syntax will be used in the whole codebase. 12 | 13 | # New Features 14 | * Added ``isawaitable`` function and deprecated ``is_async`` which will be removed 15 | in version 1.3. Issue #203 [6a2f9e4](https://github.com/quantmind/pulsar/commit/6a2f9e427ed30c55fa2dfd2c13d7e0bf1dc1d14e) 16 | * Verify parameter in http client. Fixes #191 [4611261](https://github.com/quantmind/pulsar/commit/46112618701e83f2c2de2d37391d2f5efe28b2ad) 17 | * Hindi pulsar, need double check [c83e178](https://github.com/quantmind/pulsar/commit/c83e1786a7ef7a0ef9870f59e19f9b4099673d5d) 18 | 19 | # Bug fixes and Enhancements 20 | * Versioning supports [pep440](https://www.python.org/dev/peps/pep-0440/) for alpha and beta 21 | releases too [468daa2](https://github.com/quantmind/pulsar/commit/468daa2da55ac9628215b93340d19d553e451e62) 22 | * Tidy up cython extensions. No more py2 checks. Better dev version to avoid warning 23 | in command line [6d369f0](https://github.com/quantmind/pulsar/commit/6d369f0a4f4defcbc3083769d2fb0082cc8badb3) 24 | * fix [#211](https://github.com/quantmind/pulsar/issues/211) 25 | * Fix [#209](https://github.com/quantmind/pulsar/issues/209) 26 | 27 | ### Examples 28 | * Added Green Flask example applications to illustrate how to use synchronous web frameworks and 29 | pulsar ``greenio`` module [1855819](https://github.com/quantmind/pulsar/commit/1855819ed41a7bf67b6dba0feb39d593b480bb38) 30 | 31 | ### Test 32 | * Links test [c80b163](https://github.com/quantmind/pulsar/commit/c80b163c6e5eeea3fbcb8af677d6e6a9369545da) 33 | * Don't run slugify test if unidecode package is not installed. Fixes [#206](https://github.com/quantmind/pulsar/issues/206) 34 | 35 | ### Wsgi 36 | * Check for missing suffix in the file when serving media files [bc6e04d](https://github.com/quantmind/pulsar/commit/bc6e04d805ad8579ff28f106cb1a14e4891f0ae6) 37 | 38 | -------------------------------------------------------------------------------- /docs/history/1.4.md: -------------------------------------------------------------------------------- 1 | ## Ver. 1.4.1 - 2016-Sep-09 2 | 3 | This version brings a critical bug fix in the HTTP client Digest authentication flow. 4 | If you use that feature you need to upgrade now! 5 | 6 | * Critical bug fix in HTTPDigest Authentication #243 7 | * Using pulsar with docker? bind to all network interfaces #241 8 | * Allow for True attributes in HTML content 9 | * Added HTML body scripts [bde6875](https://github.com/quantmind/pulsar/commit/bde6875d2cca7b0e21a2d358baa76a498e61b5d3) 10 | 11 | 12 | ## Ver. 1.4.0 - 2016-Aug-04 13 | 14 | A release which brings several improvements, bug fixes and a minor backward 15 | compatibility in the asynchronous Redis Lock. 16 | Importantly, the django pulse application has been moved to its own repo. 17 | 18 | * Django ``pulse`` application has been moved to its own repo [pulsar-django](https://github.com/quantmind/pulsar-django) 19 | * Refactored the close method in TcpServer 20 | * Added ``closed`` property to green pool 21 | * Better stopping of monitors 22 | * Added ``after_run`` method to actors 23 | * HttpClient bug fix #221 [9249331](https://github.com/quantmind/pulsar/commit/92493315d0559e061ca78f2c1631a4d6d34292bb) 24 | * Added asynchronous Lock primitive to the async module [4a10883](https://github.com/quantmind/pulsar/commit/4a10883aaf2ee76bf495035d6f57fe602eaede4d) 25 | * Handle datastore and redis pubsub connections drop with `connection_lost` event [dc322b7](https://github.com/quantmind/pulsar/commit/dc322b761f978cb97dabc7809545b296c737db0a) 26 | * Always decode content as json in JsonProxy (thanks to @wilddom), pull request #233 27 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Pulsar 3 | ================ 4 | 5 | 6 | Concurrent framework for Python. 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | overview 12 | design 13 | faq 14 | advantage 15 | tutorials/index 16 | api/index 17 | changelog 18 | settings 19 | internals/index 20 | acknowledgement 21 | pics 22 | 23 | .. _apps-framework: 24 | 25 | Applications 26 | ========================= 27 | 28 | Pulsar applications are built on top of pulsar concurrent framework and are 29 | located in the :mod:`pulsar.apps` module. 30 | 31 | .. toctree:: 32 | :maxdepth: 1 33 | 34 | apps/socket 35 | apps/wsgi/index 36 | apps/rpc 37 | apps/websockets 38 | apps/http 39 | apps/greenio 40 | apps/test 41 | apps/ds 42 | apps/data/index 43 | -------------------------------------------------------------------------------- /docs/internals/index.rst: -------------------------------------------------------------------------------- 1 | 2 | ================= 3 | Pulsar Internals 4 | ================= 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | actor 10 | utilities 11 | mixins 12 | -------------------------------------------------------------------------------- /docs/internals/mixins.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | .. module:: pulsar.async.mixins 5 | 6 | .. _protocol-mixins-api: 7 | 8 | Protocol Mixins 9 | ===================== 10 | 11 | FlowControl 12 | ~~~~~~~~~~~~~~~~~~ 13 | .. autoclass:: FlowControl 14 | :members: 15 | :member-order: bysource 16 | 17 | Timeout 18 | ~~~~~~~~~~~~~~ 19 | .. autoclass:: Timeout 20 | :members: 21 | :member-order: bysource 22 | -------------------------------------------------------------------------------- /docs/internals/utilities.rst: -------------------------------------------------------------------------------- 1 | .. _tools: 2 | 3 | ================== 4 | Tools 5 | ================== 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | .. _intro-overview: 2 | 3 | ================= 4 | Overview 5 | ================= 6 | 7 | .. include:: ../README.rst 8 | -------------------------------------------------------------------------------- /docs/pics.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Pics 3 | ================ 4 | 5 | Game of life 6 | ================= 7 | 8 | Did you know? 9 | 10 | Pulsar is the fourth most common oscillator in the game of life. 11 | 12 | .. image:: http://www.conwaylife.com/w/images/e/ef/Pulsar.gif 13 | :width: 100 14 | 15 | 16 | Python Powered 17 | ================= 18 | 19 | Pulsar loves python as much as the minion in the image! 20 | 21 | .. image:: https://lh5.googleusercontent.com/-xVSvSecp6kE/Uk3XDSCCHVI/AAAAAAAABIs/r1rnKMHbT_w/w1560-h878-no 22 | :width: 500 23 | 24 | 25 | Logos 26 | ======= 27 | Pulsar needs a logo, can you design one for us? 28 | 29 | Logo 1 30 | ~~~~~~~~~~ 31 | By Luca Sbardella 32 | 33 | .. image:: _static/pulsar.png 34 | :width: 200 px 35 | 36 | 37 | Logo 2 38 | ~~~~~~~~~~ 39 | By Luca Sbardella 40 | 41 | Inspired from one of the pulsar oscillator images 42 | 43 | .. image:: _static/pulsar3.png 44 | :width: 200 px 45 | 46 | 47 | Logo 3 48 | ~~~~~~~~~~ 49 | By Luca Sbardella 50 | 51 | .. image:: _static/pulsar-logo.svg 52 | :width: 200 px 53 | 54 | .. image:: _static/pulsar-banner.svg 55 | :width: 600 px 56 | 57 | Logo 4 58 | ~~~~~~~~~~ 59 | By Ralf Hölzemer 60 | 61 | A worm's eye view with 62 | a spiral to symbolize the rotational energy of a Pulsar. 63 | Creative_ commons license. 64 | 65 | .. image:: _static/pulsar_logos/pulsar_colored_logo_only.svg 66 | :width: 200 px 67 | 68 | .. image:: _static/pulsar_logos/pulsar_monochome_logo_only.svg 69 | :width: 200 px 70 | 71 | .. image:: _static/pulsar_logos/pulsar_colored_id_horizontal.svg 72 | :width: 600 px 73 | 74 | .. _Creative: http://creativecommons.org/licenses/by-nc/3.0/ 75 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | .. _settings: 2 | 3 | .. module:: pulsar.utils.config 4 | 5 | ==================== 6 | Settings 7 | ==================== 8 | 9 | This is the auto-generated full list of pulsar settings 10 | available for configuring your pulsar servers or applications. Pulsar can 11 | run several :class:`.Application` in one server, via the 12 | :class:`.MultiApp` class. 13 | 14 | Each application has its own set of configuration parameters with the 15 | only exception of the 16 | :ref:`global server settings `. 17 | 18 | 19 | .. pulsar_settings:: 20 | 21 | Utilities 22 | ============== 23 | 24 | .. autofunction:: pass_through 25 | -------------------------------------------------------------------------------- /docs/tutorials/actors.rst: -------------------------------------------------------------------------------- 1 | 2 | ================== 3 | The Arbiter 4 | ================== 5 | 6 | To use actors in pulsar you need to start the :ref:`Arbiter `, 7 | a very special actor which controls the life of all actors spawned during the 8 | execution of your code. 9 | 10 | The obtain the arbiter:: 11 | 12 | >>> from pulsar.api import arbiter 13 | >>> a = arbiter() 14 | >>> a.is_running() 15 | False 16 | 17 | Note that the arbiter is a singleton, therefore calling :func:`~.arbiter` 18 | multiple times always return the same object. 19 | 20 | To run the arbiter:: 21 | 22 | >>> a.start() 23 | 24 | This command will start the arbiter :ref:`event loop `, 25 | and therefore no more inputs are read from the command line! 26 | -------------------------------------------------------------------------------- /docs/tutorials/calculator.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-calculator: 2 | 3 | ====================== 4 | JSON-RPC Calculator 5 | ====================== 6 | 7 | The code for this example is located in the :mod:`examples.calculator.manage` 8 | module. 9 | 10 | .. automodule:: examples.calculator.manage 11 | -------------------------------------------------------------------------------- /docs/tutorials/cextensions.rst: -------------------------------------------------------------------------------- 1 | .. _cexpensions: 2 | 3 | ====================== 4 | C Extensions 5 | ====================== 6 | 7 | Pulsar is shipped with a set of C extensions for speeding up performance and 8 | provide additional functionalities. 9 | 10 | * Redis encoder/decoder 11 | * Websocket encoder/decoder 12 | -------------------------------------------------------------------------------- /docs/tutorials/chat.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-chat: 2 | 3 | ============================ 4 | Websocket Chat Server 5 | ============================ 6 | 7 | The code for this example is located in the :mod:`examples.chat.manage` module. 8 | 9 | .. automodule:: examples.chat.manage -------------------------------------------------------------------------------- /docs/tutorials/clients.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _tutorials-writing-clients: 3 | 4 | ========================== 5 | Echo Server and Client 6 | ========================== 7 | 8 | .. automodule:: examples.echo.manage 9 | 10 | -------------------------------------------------------------------------------- /docs/tutorials/command.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Using command 3 | ================ 4 | 5 | We take a look on how to spawn an actor which echoes us when 6 | we send it a message. 7 | The first method for achieving this is to write an 8 | :ref:`actor command `:: 9 | 10 | from asyncio import ensure_future 11 | 12 | from pulsar.api import arbiter, command, spawn, send 13 | 14 | 15 | names = ['john', 'luca', 'carl', 'jo', 'alex'] 16 | 17 | 18 | @command() 19 | def greetme(request, message): 20 | echo = 'Hello {}!'.format(message['name']) 21 | request.actor.logger.info(echo) 22 | return echo 23 | 24 | 25 | class Greeter: 26 | 27 | def __init__(self): 28 | a = arbiter() 29 | self._loop = a._loop 30 | self._loop.call_later(1, self) 31 | a.start() 32 | 33 | def __call__(self, a=None): 34 | ensure_future(self._work(a)) 35 | 36 | async def _work(self, a=None): 37 | if a is None: 38 | a = await spawn(name='greeter') 39 | if names: 40 | name = names.pop() 41 | await send(a, 'greetme', {'name': name}) 42 | self._loop.call_later(1, self, a) 43 | else: 44 | arbiter().stop() 45 | 46 | if __name__ == '__main__': 47 | Greeter() 48 | -------------------------------------------------------------------------------- /docs/tutorials/deploy.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-deploy: 2 | 3 | =========================== 4 | Deploy Web Applications 5 | =========================== 6 | 7 | Pulsar has a fully fledged wsgi http server which can used to serve dynamic 8 | web applications. The best way to do that is to run pulsar server behind 9 | another HTTP server such as nginx_ and apache_. 10 | 11 | Nginx 12 | ========== 13 | 14 | The best way to deploy web apps using pulsar wsgi http server is 15 | to use nginx as reverse proxy server to pulsar server. 16 | 17 | For example, this minimal configuration does the trick:: 18 | 19 | server { 20 | listen 80; 21 | server_name example.org; 22 | access_log /var/log/nginx/example.log; 23 | 24 | location / { 25 | proxy_pass http://127.0.0.1:8060; 26 | proxy_set_header Host $host; 27 | proxy_set_header X-Real-IP $remote_addr; 28 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 29 | proxy_redirect off; 30 | } 31 | } 32 | 33 | 34 | Link the configuration file to the nginx site_enabled directory 35 | (usually in ``/etc/nginx/site_enabled``):: 36 | 37 | sudo ln -s site.conf /etc/nginx/sites-enabled/site.conf 38 | 39 | and `reload nginx`_:: 40 | 41 | nginx -s reload 42 | 43 | Apache 44 | ========= 45 | 46 | An alternative option to nginx is to use apache_ web server. 47 | Apache is a complex server with lots of configuration parameters to fine tune 48 | how it works. 49 | 50 | To use pulsar behind apache you need the mod_proxy_ installed. 51 | 52 | 53 | .. _nginx: http://nginx.org/en/ 54 | .. _apache: http://httpd.apache.org/ 55 | .. _mod_proxy: http://httpd.apache.org/docs/current/mod/mod_proxy.html 56 | .. _`reload nginx`: http://wiki.nginx.org/CommandLine 57 | -------------------------------------------------------------------------------- /docs/tutorials/events.rst: -------------------------------------------------------------------------------- 1 | .. _event-handling: 2 | 3 | ======================= 4 | Events 5 | ======================= 6 | 7 | Event handling is implemented via the :class:`.Event` and :class:`.OneTime` 8 | classes. In addition the :class:`.EventHandler` provide a mixin which can 9 | be used to attach events to a class. Event are implemented in the 10 | :mod:`pulsar.async.events` module. 11 | 12 | .. _many-times-event: 13 | 14 | Many times event 15 | ==================== 16 | 17 | An event handler is created simply:: 18 | 19 | event = Event() 20 | 21 | You can bind callbacks to an event:: 22 | 23 | def say_hello(arg, **kw): 24 | print('Hello %s!') 25 | 26 | event.bind(say_hello) 27 | 28 | and you can fire it:: 29 | 30 | >>> event.fire(None) 31 | Hello None! 32 | >>> event.fire('luca') 33 | Hello luca! 34 | 35 | An :class:`.Event` can be fired as many times as you like and therefore we 36 | referred to this type of event as a **may times event**. 37 | 38 | 39 | .. _one-time-event: 40 | 41 | One time event 42 | ===================== 43 | As the name says, the :class:`.OneTime` is a special event which 44 | can be fired once only. Firing a :class:`.OneTime` event multiple 45 | times will cause an :class:`~asyncio.InavlidStateError` to be raised. 46 | 47 | A :class:`.OneTime` event is a specialised :class:`~asyncio.Future` and 48 | therefore you can yield it in a coroutine to wait until it get fired. 49 | 50 | 51 | Event handler 52 | ================= 53 | The :class:`.EventHandler` mixin adds event handling methods to python classes:: 54 | 55 | import pulsar 56 | 57 | class Events(pulsar.EventHandler): 58 | ONE_TIME_EVENTS = ('start', 'finish') 59 | MANY_TIMES_EVENTS = ('data') 60 | 61 | 62 | To fire an event, one uses the :meth:`~.EventHandler.fire_event` method with 63 | first positional argument given by the event name:: 64 | 65 | >> o = Events() 66 | >> o.fire_event('start') 67 | 68 | Optionally, it is possible to pass one additional parameter:: 69 | 70 | >> o.fire_event('start', 'hello') 71 | 72 | Adding event callbacks is done via the :meth:`~.Event.bind` 73 | method. The method accept two parameters, the event name and a callable 74 | accepting one parameters, the caller which fires the event or the 75 | optional positional parameter passed to the :meth:`~.EventHandler.fire_event` 76 | method mentioned above:: 77 | 78 | def start_handler(result): 79 | ... 80 | 81 | o.event('start').bind(start_handler) 82 | -------------------------------------------------------------------------------- /docs/tutorials/flask.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-flask: 2 | 3 | ==================== 4 | Flask Application 5 | ==================== 6 | 7 | The code for this example is located in the :mod:`examples.flaskapp.manage` 8 | module. 9 | 10 | .. automodule:: examples.flaskapp.manage 11 | -------------------------------------------------------------------------------- /docs/tutorials/hello.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-hello-world: 2 | 3 | ================== 4 | Hello World! 5 | ================== 6 | 7 | The code for this example is located in the :mod:`examples.helloworld.manage` 8 | module. 9 | 10 | .. automodule:: examples.helloworld.manage -------------------------------------------------------------------------------- /docs/tutorials/httpbin.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-httpbin: 2 | 3 | ================== 4 | Http Bin 5 | ================== 6 | 7 | The code for this example is located in the :mod:`examples.httpbin.manage` 8 | module. 9 | 10 | .. automodule:: examples.httpbin.manage -------------------------------------------------------------------------------- /docs/tutorials/index.rst: -------------------------------------------------------------------------------- 1 | .. module:: examples 2 | 3 | .. _tutorials: 4 | 5 | ===================== 6 | Tutorials & Topics 7 | ===================== 8 | 9 | 10 | Tutorials 11 | ============== 12 | 13 | Most of the material presented in the these tutorials is based on the 14 | :mod:`examples` module in the `pulsar distribution `_. 15 | 16 | .. toctree:: 17 | :maxdepth: 1 18 | 19 | hello 20 | clients 21 | wsgi 22 | deploy 23 | httpbin 24 | proxy 25 | calculator 26 | chat 27 | flask 28 | philosophers 29 | pulsards 30 | udp 31 | logging 32 | 33 | 34 | Topics 35 | ============== 36 | 37 | These sections cover **pulsar internals** for the curious reader. 38 | 39 | .. toctree:: 40 | :maxdepth: 1 41 | 42 | actors 43 | command 44 | messages 45 | events 46 | signal 47 | sync 48 | benchmarking 49 | cextensions 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/tutorials/logging.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-logging: 2 | 3 | ================== 4 | Logging 5 | ================== 6 | 7 | Pulsar provides several :ref:`settings ` for managing the python 8 | logging module and display information when running. 9 | These configuration parameters can be specified both on the command line 10 | or in the :ref:`config ` file of your application. 11 | 12 | The :ref:`log-level ` sets levels of loggers, for example:: 13 | 14 | python script.py --log-level debug 15 | 16 | Set the log level for the ``pulsar.*`` loggers to ``DEBUG``. 17 | 18 | You can pass several namespaces to the command, for example:: 19 | 20 | python script.py --log-level warning pulsar.wsgi.debug 21 | -------------------------------------------------------------------------------- /docs/tutorials/messages.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-messages: 2 | 3 | ======================= 4 | Actor messages 5 | ======================= 6 | 7 | .. automodule:: pulsar.async.mailbox -------------------------------------------------------------------------------- /docs/tutorials/philosophers.rst: -------------------------------------------------------------------------------- 1 | .. _dining-philosophers: 2 | 3 | ======================== 4 | Dining Philosophers 5 | ======================== 6 | 7 | The code for this example is located in the :mod:`examples.philosophers.manage` 8 | module. 9 | 10 | .. automodule:: examples.philosophers.manage 11 | -------------------------------------------------------------------------------- /docs/tutorials/proxy.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-proxy-server: 2 | 3 | ================== 4 | HTTP Proxy Server 5 | ================== 6 | 7 | The code for this example is located in the :mod:`examples.proxyserver.manage` 8 | module. 9 | 10 | .. automodule:: examples.proxyserver.manage 11 | 12 | -------------------------------------------------------------------------------- /docs/tutorials/pulsards.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-pulsards: 2 | 3 | ============================ 4 | Pulsar-ds 5 | ============================ 6 | 7 | The code for this example is located in the :mod:`examples.pulsards.manage` 8 | module. 9 | 10 | .. automodule:: examples.pulsards.manage 11 | -------------------------------------------------------------------------------- /docs/tutorials/signal.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-signal: 2 | 3 | ======================= 4 | Signal Handling 5 | ======================= 6 | 7 | 8 | Handling of SIGTERM 9 | ========================= 10 | The SIGTERM signals tells pulsar to shutdown gracefully. When this signal is 11 | received, the arbiter schedules a shutdown very similar to the one performed 12 | when the :ref:`stop command ` is called. 13 | The scheduled shutdown starts ASAP, -------------------------------------------------------------------------------- /docs/tutorials/sync.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials-synchronous: 2 | 3 | ========================= 4 | Synchronous Components 5 | ========================= 6 | 7 | :class:`.AsyncObject` are used everywhere in pulsar. 8 | These objects expose the ``_loop`` attribute as discussed in the 9 | :ref:`design documentation `. 10 | 11 | Normally, the ``_loop`` is a running event loop controlled by an 12 | :class:`.Actor`. 13 | In this case, all operations which requires the loop, are carried out 14 | asynchronously as one would expect. 15 | 16 | However, sometimes it can be useful to have 17 | :class:`.AsyncObject` which behaves in a synchronous fashion. 18 | Pulsar achieves this by using a new event loop for that object. 19 | For example, this statement creates a synchronous :class:`.HttpClient`:: 20 | 21 | >>> asyncio import new_event_loop 22 | >>> from pulsar.apps import http 23 | >>> client = http.HttpClient(loop=new_event_loop()) 24 | >>> client._loop.is_running() 25 | False 26 | 27 | You can now execute synchronous requests:: 28 | 29 | >>> response = client.get('https://pypi.python.org/pypi/pulsar/json') 30 | 31 | Under the hood, pulsar treats a synchronous request exactly in the same way as 32 | an asynchronous one with the only difference that the 33 | :ref:`event loop ` 34 | is always used via the :meth:`~.asyncio.BaseEventLoop.run_until_complete` 35 | method to wait for the response to be available. 36 | -------------------------------------------------------------------------------- /docs/tutorials/udp.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _tutorials-udp: 3 | 4 | ========================== 5 | Udp Server and Client 6 | ========================== 7 | 8 | .. automodule:: examples.echoudp.manage 9 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/__init__.py -------------------------------------------------------------------------------- /examples/calculator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/calculator/__init__.py -------------------------------------------------------------------------------- /examples/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/chat/__init__.py -------------------------------------------------------------------------------- /examples/chat/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Websocket example in pulsar 6 | 43 | 44 | 45 | 46 |
47 | 48 | 49 |
50 |
51 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /examples/chat/media/chat.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | $(document).ready(function() { 4 | 5 | }); 6 | }(jQuery)); -------------------------------------------------------------------------------- /examples/echo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/echo/__init__.py -------------------------------------------------------------------------------- /examples/echoudp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/echoudp/__init__.py -------------------------------------------------------------------------------- /examples/echoudp/manage.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example illustrates how to write a UDP Echo server and client pair. 3 | The code for this example is located in the :mod:`examples.echoudp.manage` 4 | module. 5 | 6 | Run The example 7 | ==================== 8 | 9 | To run the server:: 10 | 11 | python manage.py 12 | 13 | Open a new shell, in this directory, launch python and type:: 14 | 15 | >>> from manage import Echo 16 | >>> echo = Echo(('localhost', 8060)) 17 | >>> echo(b'Hello!\\n') 18 | b'Hello!' 19 | 20 | Writing the Client 21 | ========================= 22 | 23 | The first step is to write a small class handling a connection 24 | pool with the remote server. The :class:`Echo` class does just that, 25 | it subclass the handy :class:`.AbstractUdpClient` and uses 26 | the asynchronous :class:`.Pool` of connections as backbone. 27 | 28 | The second step is the implementation of the :class:`EchoUdpProtocol`, 29 | a subclass of :class:`.DatagramProtocol`. 30 | The :class:`EchoUdpProtocol` is needed for two reasons: 31 | 32 | * It encodes and sends the request to the remote server 33 | * It listens for incoming data from the remote server via the 34 | :meth:`~EchoUdpProtocol.datagram_received` method. 35 | 36 | 37 | Echo Server 38 | ================== 39 | 40 | .. autofunction:: server 41 | 42 | """ 43 | from pulsar.api import DatagramProtocol 44 | from pulsar.apps.socket import UdpSocketServer 45 | 46 | from examples.echo.manage import EchoServerProtocol, Echo as EchoTcp 47 | 48 | 49 | class Echo(EchoTcp): 50 | '''A client for the Echo UDP server. 51 | ''' 52 | protocol_type = DatagramProtocol 53 | 54 | def connect(self): 55 | return self.create_datagram_endpoint(remote_addr=self.address) 56 | 57 | 58 | def server(name=None, description=None, **kwargs): 59 | '''Create the :class:`.UdpSocketServer`. 60 | ''' 61 | return UdpSocketServer( 62 | EchoServerProtocol, 63 | name=name or 'echoudpserver', 64 | description=description or 'Echo Udp Server', 65 | **kwargs 66 | ) 67 | 68 | 69 | if __name__ == '__main__': # pragma nocover 70 | server().start() 71 | -------------------------------------------------------------------------------- /examples/echoudp/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.api import send, get_application 4 | from pulsar.apps.test import run_test_server 5 | 6 | from examples.echoudp.manage import server, Echo 7 | 8 | 9 | class A(unittest.TestLoader): 10 | # class TestEchoUdpServerThread(unittest.TestCase): 11 | concurrency = 'process' 12 | app_cfg = None 13 | 14 | @classmethod 15 | async def setUpClass(cls): 16 | await run_test_server(cls, server) 17 | cls.client = Echo(cls.app_cfg.addresses[0]) 18 | 19 | @classmethod 20 | def tearDownClass(cls): 21 | if cls.app_cfg: 22 | return send('arbiter', 'kill_actor', cls.app_cfg.name) 23 | 24 | # TEST THE SERVER APPLICATION 25 | async def test_server_on_arbiter(self): 26 | app = await get_application(self.app_cfg.name) 27 | cfg = app.cfg 28 | self.assertTrue(cfg.addresses) 29 | self.assertTrue(cfg.address) 30 | self.assertNotEqual(cfg.addresses[0], cfg.address) 31 | 32 | def test_server(self): 33 | server = self.app_cfg.app() 34 | self.assertTrue(server) 35 | self.assertTrue(server.cfg.addresses) 36 | 37 | # TEST CLIENT INTERACTION 38 | async def test_ping(self): 39 | result = await self.client(b'ciao luca') 40 | self.assertEqual(result, b'ciao luca') 41 | 42 | async def test_large(self): 43 | '''Echo a 3MB message''' 44 | msg = b''.join((b'a' for x in range(2**13))) 45 | result = await self.client(msg) 46 | self.assertEqual(result, msg) 47 | -------------------------------------------------------------------------------- /examples/flaskapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/flaskapp/__init__.py -------------------------------------------------------------------------------- /examples/flaskapp/manage.py: -------------------------------------------------------------------------------- 1 | """Simple Flask application served by pulsar WSGI server on a pool of threads 2 | """ 3 | from flask import Flask, make_response 4 | 5 | from pulsar.apps import wsgi 6 | 7 | 8 | def FlaskApp(): 9 | app = Flask(__name__) 10 | 11 | @app.errorhandler(404) 12 | def not_found(e): 13 | return make_response("404 Page", 404) 14 | 15 | @app.route('/', methods=['GET']) 16 | def add_org(): 17 | return "Flask Example" 18 | 19 | return app 20 | 21 | 22 | class Site(wsgi.LazyWsgi): 23 | 24 | def setup(self, environ=None): 25 | app = FlaskApp() 26 | return wsgi.WsgiHandler((wsgi.wait_for_body_middleware, 27 | wsgi.middleware_in_executor(app))) 28 | 29 | 30 | def server(**kwargs): 31 | return wsgi.WSGIServer(Site(), **kwargs) 32 | 33 | 34 | if __name__ == '__main__': # pragma nocover 35 | server().start() 36 | -------------------------------------------------------------------------------- /examples/flaskapp/tests.py: -------------------------------------------------------------------------------- 1 | '''Tests the "flaskapp" example.''' 2 | import unittest 3 | 4 | from pulsar import SERVER_SOFTWARE 5 | from pulsar.api import send 6 | from pulsar.apps.http import HttpClient 7 | from pulsar.apps.test import run_test_server 8 | 9 | try: 10 | from examples.flaskapp.manage import server 11 | except ImportError: 12 | server = None 13 | 14 | 15 | @unittest.skipUnless(server, "Requires flask module") 16 | class TestFlaskApp(unittest.TestCase): 17 | app_cfg = None 18 | 19 | @classmethod 20 | async def setUpClass(cls): 21 | await run_test_server(cls, server) 22 | cls.client = HttpClient() 23 | 24 | @classmethod 25 | def tearDownClass(cls): 26 | if cls.app_cfg is not None: 27 | return send('arbiter', 'kill_actor', cls.app_cfg.name) 28 | 29 | async def test_response_200(self): 30 | c = self.client 31 | response = await c.get(self.uri) 32 | self.assertEqual(response.status_code, 200) 33 | content = response.content 34 | self.assertEqual(content, b'Flask Example') 35 | headers = response.headers 36 | self.assertTrue(headers) 37 | self.assertEqual(headers['server'], SERVER_SOFTWARE) 38 | 39 | async def test_response_404(self): 40 | c = self.client 41 | response = await c.get('%s/bh' % self.uri) 42 | self.assertEqual(response.status_code, 404) 43 | self.assertEqual(response.content, b'404 Page') 44 | -------------------------------------------------------------------------------- /examples/helloworld/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/helloworld/__init__.py -------------------------------------------------------------------------------- /examples/helloworld/manage.py: -------------------------------------------------------------------------------- 1 | """This example is a simple WSGI_ script which displays 2 | the ``Hello World!`` message. To run the script type:: 3 | 4 | python manage.py 5 | 6 | To see all options available type:: 7 | 8 | python manage.py -h 9 | 10 | .. autofunction:: hello 11 | 12 | .. autofunction:: server 13 | 14 | .. _WSGI: http://www.python.org/dev/peps/pep-3333/ 15 | """ 16 | from pulsar.api import MethodNotAllowed 17 | from pulsar.apps import wsgi 18 | 19 | 20 | def hello(environ, start_response): 21 | '''The WSGI_ application handler which returns an iterable 22 | over the "Hello World!" message.''' 23 | if environ['REQUEST_METHOD'] == 'GET': 24 | data = b'Hello World!\n' 25 | status = '200 OK' 26 | response_headers = [ 27 | ('Content-type', 'text/plain'), 28 | ('Content-Length', str(len(data))) 29 | ] 30 | start_response(status, response_headers) 31 | return iter([data]) 32 | else: 33 | raise MethodNotAllowed 34 | 35 | 36 | def server(description=None, **kwargs): 37 | '''Create the :class:`.WSGIServer` running :func:`hello`.''' 38 | description = description or 'Pulsar Hello World Application' 39 | return wsgi.WSGIServer(hello, description=description, **kwargs) 40 | 41 | 42 | if __name__ == '__main__': # pragma nocover 43 | server().start() 44 | -------------------------------------------------------------------------------- /examples/helloworld/tests.py: -------------------------------------------------------------------------------- 1 | '''Tests the "helloworld" example.''' 2 | import unittest 3 | 4 | from pulsar import SERVER_SOFTWARE 5 | from pulsar.api import send, get_application, get_actor 6 | from pulsar.apps.http import HttpClient 7 | from pulsar.apps.test import dont_run_with_thread, run_test_server 8 | 9 | from examples.helloworld.manage import server 10 | 11 | 12 | class TestHelloWorldThread(unittest.TestCase): 13 | app_cfg = None 14 | concurrency = 'thread' 15 | 16 | @classmethod 17 | async def setUpClass(cls): 18 | await run_test_server(cls, server) 19 | cls.client = HttpClient() 20 | 21 | @classmethod 22 | def tearDownClass(cls): 23 | if cls.app_cfg is not None: 24 | return send('arbiter', 'kill_actor', cls.app_cfg.name) 25 | 26 | async def testMeta(self): 27 | app = await get_application(self.app_cfg.name) 28 | self.assertEqual(app.name, self.app_cfg.name) 29 | monitor = get_actor().get_actor(app.name) 30 | self.assertTrue(monitor.is_running()) 31 | self.assertEqual(app, monitor.app) 32 | self.assertEqual(str(app), app.name) 33 | self.assertEqual(app.cfg.bind, '127.0.0.1:0') 34 | 35 | async def testResponse(self): 36 | c = self.client 37 | response = await c.get(self.uri) 38 | self.assertEqual(response.status_code, 200) 39 | content = response.content 40 | self.assertEqual(content, b'Hello World!\n') 41 | headers = response.headers 42 | self.assertTrue(headers) 43 | self.assertEqual(headers['content-type'], 'text/plain') 44 | self.assertEqual(headers['server'], SERVER_SOFTWARE) 45 | 46 | async def testTimeIt(self): 47 | c = self.client 48 | b = await c.timeit('get', 5, self.uri) 49 | self.assertTrue(b.taken >= 0) 50 | 51 | async def test405(self): 52 | c = self.client 53 | response = await c.post(self.uri, data={'bla': 'foo'}) 54 | self.assertEqual(response.status_code, 405) 55 | 56 | 57 | @dont_run_with_thread 58 | class TestHelloWorldProcess(TestHelloWorldThread): 59 | concurrency = 'process' 60 | -------------------------------------------------------------------------------- /examples/httpbin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/httpbin/__init__.py -------------------------------------------------------------------------------- /examples/httpbin/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/httpbin/assets/favicon.ico -------------------------------------------------------------------------------- /examples/httpbin/assets/httpbin.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | body { 6 | color: #222; 7 | background: #F0F9FF; 8 | font-family: Optima,Segoe,"Segoe UI",Candara,Calibri,Arial,sans-serif; 9 | font-size: 17px; 10 | padding: 10px; 11 | line-height: 24px; 12 | } 13 | h1 a, h1 a:visited { 14 | text-decoration: none; 15 | color: #003366; 16 | } 17 | a, a:visited { 18 | color: #008AE6; 19 | } 20 | h1 a:hover, a:hover { 21 | color: #AD5700; 22 | } 23 | ul { 24 | list-style: disc outside none; 25 | margin: 0 0 9px 25px; 26 | padding: 0; 27 | } -------------------------------------------------------------------------------- /examples/httpbin/assets/httpbin.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | function ajax_request(method) { 3 | return function (e) { 4 | e.preventDefault(); 5 | $.ajax({ 6 | url: e.target.href, 7 | type: method, 8 | data: { 9 | body: 'this is a message', 10 | type: method, 11 | }, 12 | success: function (data, status) { 13 | alert('Got succesfull response: ' + data['method']); 14 | } 15 | }); 16 | } 17 | } 18 | $(document).ready(function() { 19 | $.each(['post', 'delete', 'put', 'patch'], function (i, name) { 20 | $('a.' + name).click(ajax_request(name)); 21 | }); 22 | }); 23 | }(jQuery)); -------------------------------------------------------------------------------- /examples/httpbin/assets/stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Httpbin server stats 6 | 7 | 8 | 56 | 57 | 58 |

Plot

59 |
60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /examples/httpbin/assets/template.html: -------------------------------------------------------------------------------- 1 |

%s   %s   %s

2 |

HTTP server for testing clients. 3 | Powered by pulsar %s.

4 |

Running on Python %s

5 | %s 6 | -------------------------------------------------------------------------------- /examples/httpbin/assets/websocket.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Websocket example in pulsar 6 | 7 | 8 | 56 | 57 | 58 |

Plot

59 |
60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /examples/httpbin/config.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def post_request(response, exc=None): 4 | '''Handle a post request, ``response`` is an instance of 5 | :class:`.HttpServerResponse` 6 | ''' 7 | pass 8 | -------------------------------------------------------------------------------- /examples/httpbin/httpbin.conf: -------------------------------------------------------------------------------- 1 | # nginx configuration file for a pulsar server 2 | # 3 | server { 4 | # listen 80 default deferred; # for Linux 5 | # listen 80 default accept_filter=httpready; # for FreeBSD 6 | listen 80 default; 7 | 8 | client_max_body_size 4G; 9 | server_name _; 10 | 11 | # ~2 seconds is often enough for most folks to parse HTML/CSS and 12 | # retrieve needed images/icons/frames, connections are cheap in 13 | # nginx so increasing this is generally safe... 14 | keepalive_timeout 15; 15 | 16 | # path for static files 17 | root /path/to/app/current/public; 18 | 19 | location / { 20 | # an HTTP header important enough to have its own Wikipedia entry: 21 | # http://en.wikipedia.org/wiki/X-Forwarded-For 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | 24 | # enable this if and only if you use HTTPS, this helps Rack 25 | # set the proper protocol for doing redirects: 26 | # proxy_set_header X-Forwarded-Proto https; 27 | 28 | # pass the Host: header from the client right along so redirects 29 | # can be set properly within the Rack application 30 | proxy_set_header Host $http_host; 31 | 32 | # we don't want nginx trying to do something clever with 33 | # redirects, we set the Host: header above already. 34 | proxy_redirect off; 35 | 36 | # set "proxy_buffering off" *only* for Rainbows! when doing 37 | # Comet/long-poll stuff. It's also safe to set if you're 38 | # using only serving fast clients with Unicorn + nginx. 39 | # Otherwise you _want_ nginx to buffer responses to slow 40 | # clients, really. 41 | # proxy_buffering off; 42 | 43 | # Try to serve static files from nginx, no point in making an 44 | # *application* server like pulsar serve static files. 45 | if (!-f $request_filename) { 46 | proxy_pass http://app_server; 47 | break; 48 | } 49 | } 50 | 51 | # Error pages 52 | error_page 500 502 503 504 /500.html; 53 | location = /500.html { 54 | root /path/to/app/current/public; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/httpbin/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICJzCCAZACCQCFmwKKTa6kazANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJH 3 | QjETMBEGA1UECBMKU29tZS1TdGF0ZTEPMA0GA1UEBxMGTG9uZG9uMRIwEAYDVQQK 4 | EwlxdWFudG1pbmQxDzANBgNVBAMTBnB1bHNhcjAeFw0xMzA4MjExNTA5MDhaFw0y 5 | MzA4MTkxNTA5MDhaMFgxCzAJBgNVBAYTAkdCMRMwEQYDVQQIEwpTb21lLVN0YXRl 6 | MQ8wDQYDVQQHEwZMb25kb24xEjAQBgNVBAoTCXF1YW50bWluZDEPMA0GA1UEAxMG 7 | cHVsc2FyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYfp89HNzsnfsBjFh7 8 | I4DrXFbx92wuQwIZB54OtefJPMoxVW+L+eneKwE/KN5QKESQudLmyzFlfdDsnlgc 9 | h4rIFNTt/F4LE1qD3/9XP+jrVaeKWDrcEOTTQxQWt+pfEUOHkJ5Na1tsgdW9nqoD 10 | FPo6PZixBTSFDIRJjezOIY5cbwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAAJw5RQ6 11 | 3roKK/1N32wLu25xNMR8Xmdyuq9dZyGHgrPILqh7MHDoHTCBI4oYAj+0+Ra7KSXI 12 | QjcvfjHY/TIa1di0tcMl1k8lvyCExlmDUc0fGdHeuvgB58Zr9oaSpFgtfY4YmC2L 13 | FbzaVnodId9mn5EL1rplCnhtm9/BKT3MjvAm 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /examples/httpbin/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQCYfp89HNzsnfsBjFh7I4DrXFbx92wuQwIZB54OtefJPMoxVW+L 3 | +eneKwE/KN5QKESQudLmyzFlfdDsnlgch4rIFNTt/F4LE1qD3/9XP+jrVaeKWDrc 4 | EOTTQxQWt+pfEUOHkJ5Na1tsgdW9nqoDFPo6PZixBTSFDIRJjezOIY5cbwIDAQAB 5 | AoGAKCZWeLmrtSTXHI4+EZXEcLNsNOLm+bssxzhJNihQsZwuxBRxfRI8sAD0oBN7 6 | jPE9NDmovJlNFSKuKk56dnB9ak50udEgT3ez+C5cVAbpTnZ3R+SQJBopU+eNfkeX 7 | DdihfB/x8vM3In2pidVMZqUnDbxGJmq5Zg0hbWrOjolpJZECQQDLGaKzV3fotQt5 8 | ZJGCTyHNl0gIubMBM0dWrp8rdZD7/IXlqH9MSYLu9n++2KwJsEbaKrYLj9J4+SVA 9 | 5cQvfl7pAkEAwDayvabbcuCyzmvaty+/LdzcX3AthcTkYQxRej9IuXcu/HwEBOXD 10 | lL2whEJz4KswerWdUMtdPrt0l2Mm9z65lwJAdGFiPAZZLb3gr1UTlGon4plXq7hN 11 | fNiXfnJdvxeFVv/i8jDVkS9mmewiU4fxPTQHxahH3OQzJSMPV9rRyq1NUQJAMtwG 12 | TZJiDLKR/XaiZ8hVgNAo64PmN7gSae91e7BVEOYNY7d45MbbCndqxoRC3xbM/Bsk 13 | JiW5ZYj6X7hXriJFvwJBALnpD1CGQf1QvZ7qqAzPQ/cyDdy95gf3vc0mDRcm2iD7 14 | wyKKVmc6mzC5s4LVAH5rUH1ZsmYHvj6fZ/+fZxG6s8Q= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /examples/philosophers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/philosophers/__init__.py -------------------------------------------------------------------------------- /examples/philosophers/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | 4 | from pulsar.api import send 5 | from pulsar.apps.test import test_timeout 6 | 7 | from examples.philosophers.manage import DiningPhilosophers 8 | 9 | 10 | class TestPhilosophers(unittest.TestCase): 11 | """Integration tests for the philosophers app 12 | """ 13 | app_cfg = None 14 | 15 | @classmethod 16 | async def setUpClass(cls): 17 | app = DiningPhilosophers(name='plato', parse_console=False) 18 | cls.app_cfg = await send('arbiter', 'run', app) 19 | 20 | @test_timeout(60) 21 | async def test_info(self): 22 | while True: 23 | await asyncio.sleep(0.5) 24 | info = await send('plato', 'info') 25 | all = [] 26 | for data in info.get('workers', []): 27 | p = data.get('philosopher') 28 | if p and p.get('eaten', 0) > 0: 29 | all.append(p) 30 | if len(all) == 5: 31 | break 32 | 33 | @classmethod 34 | def tearDownClass(cls): 35 | if cls.app_cfg is not None: 36 | return send('arbiter', 'kill_actor', cls.app_cfg.name) 37 | -------------------------------------------------------------------------------- /examples/proxyserver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/proxyserver/__init__.py -------------------------------------------------------------------------------- /examples/pulsarapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/pulsarapp/__init__.py -------------------------------------------------------------------------------- /examples/pulsarapp/manage.py: -------------------------------------------------------------------------------- 1 | from pulsar.apps.wsgi import Router 2 | from pulsar.apps.wsgi.handlers import WsgiHandler 3 | from pulsar.apps.wsgi import WSGIServer, WsgiResponse 4 | 5 | 6 | blueprint = Router('/') 7 | 8 | 9 | @blueprint.router('sync', methods=['get', 'post']) 10 | def sync_case(request): 11 | return WsgiResponse(200, 'sync') 12 | 13 | 14 | @blueprint.router('async', methods=['delete', 'put']) 15 | async def async_cast(request): 16 | return WsgiResponse(200, 'async') 17 | 18 | 19 | def server(**kwargs): 20 | return WSGIServer(callable=WsgiHandler((blueprint, )), **kwargs) 21 | 22 | 23 | if __name__ == '__main__': # pragma nocover 24 | server().start() 25 | -------------------------------------------------------------------------------- /examples/pulsarapp/tests.py: -------------------------------------------------------------------------------- 1 | '''Tests the "pulsarapp" example.''' 2 | import unittest 3 | 4 | from pulsar import SERVER_SOFTWARE 5 | from pulsar.api import send 6 | from pulsar.apps.http import HttpClient 7 | from pulsar.apps.test import run_test_server 8 | 9 | try: 10 | from examples.pulsarapp.manage import server 11 | except ImportError: 12 | server = None 13 | 14 | 15 | class TestPulsarApp(unittest.TestCase): 16 | app_cfg = None 17 | 18 | @classmethod 19 | async def setUpClass(cls): 20 | await run_test_server(cls, server) 21 | cls.client = HttpClient() 22 | 23 | @classmethod 24 | def tearDownClass(cls): 25 | if cls.app_cfg is not None: 26 | return send('arbiter', 'kill_actor', cls.app_cfg.name) 27 | 28 | async def test_response_200(self): 29 | c = self.client 30 | response = await c.get(self.uri + '/sync') 31 | self.assertEqual(response.status_code, 200) 32 | content = response.content 33 | self.assertEqual(content, b'sync') 34 | headers = response.headers 35 | self.assertTrue(headers) 36 | self.assertEqual(headers['server'], SERVER_SOFTWARE) 37 | 38 | response = await c.put(self.uri + '/async') 39 | self.assertEqual(response.status_code, 200) 40 | content = response.content 41 | self.assertEqual(content, b'async') 42 | headers = response.headers 43 | self.assertTrue(headers) 44 | self.assertEqual(headers['server'], SERVER_SOFTWARE) 45 | 46 | async def test_response_404(self): 47 | c = self.client 48 | response = await c.get('%s/bh' % self.uri) 49 | self.assertEqual(response.status_code, 404) 50 | -------------------------------------------------------------------------------- /examples/pulsards/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/pulsards/__init__.py -------------------------------------------------------------------------------- /examples/pulsards/manage.py: -------------------------------------------------------------------------------- 1 | '''\ 2 | Pulsar key-value store server. To run the server type:: 3 | 4 | python manage.py 5 | 6 | Open a new shell and launch python and type:: 7 | 8 | >>> from pulsar.apps.data import create_store 9 | >>> store = create_store('pulsar://localhost:6410') 10 | >>> client = store.client() 11 | >>> client.ping() 12 | True 13 | >>> client.echo('Hello!') 14 | b'Hello!' 15 | >>> client.set('bla', 'foo') 16 | True 17 | >>> client.get('bla') 18 | b'foo' 19 | >>> client.dbsize() 20 | 1 21 | 22 | ''' 23 | from pulsar.apps.ds import PulsarDS 24 | 25 | 26 | if __name__ == '__main__': # pragma nocover 27 | PulsarDS().start() 28 | -------------------------------------------------------------------------------- /examples/snippets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/snippets/__init__.py -------------------------------------------------------------------------------- /examples/snippets/actor1.py: -------------------------------------------------------------------------------- 1 | """Simple actor message passing""" 2 | from pulsar.api import arbiter, spawn, send, ensure_future, Config 3 | 4 | 5 | def start(arb): 6 | ensure_future(app()) 7 | 8 | 9 | async def app(): 10 | # Spawn a new actor 11 | proxy = await spawn(name='actor1') 12 | print(proxy.name) 13 | # Execute inner method in actor1 14 | result = await send(proxy, 'run', inner_method) 15 | print(result) 16 | 17 | await send(proxy, 'run', set_value, 10) 18 | value = await send(proxy, 'run', get_value) 19 | print(value) 20 | 21 | # Stop the application 22 | arbiter().stop() 23 | 24 | 25 | def inner_method(actor): 26 | return 42 27 | 28 | 29 | def set_value(actor, value): 30 | actor.saved_value = value 31 | 32 | 33 | def get_value(actor): 34 | return actor.saved_value 35 | 36 | 37 | if __name__ == '__main__': 38 | cfg = Config() 39 | cfg.parse_command_line() 40 | arbiter(cfg=cfg, start=start).start() 41 | -------------------------------------------------------------------------------- /examples/snippets/greeter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from pulsar.api import arbiter, command, Config 4 | 5 | 6 | names = ['john', 'luca', 'carl', 'jo', 'alex'] 7 | 8 | 9 | @command() 10 | def greetme(request, message): 11 | echo = 'Hello {}!'.format(message['name']) 12 | request.actor.logger.warning(echo) 13 | return echo 14 | 15 | 16 | class Greeter: 17 | 18 | def __call__(self, arb): 19 | self._greater_task = arb._loop.create_task(self._work(arb)) 20 | 21 | async def _work(self, arb): 22 | a = await arb.spawn(name='greeter') 23 | while names: 24 | name = names.pop() 25 | await arb.send(a, 'greetme', {'name': name}) 26 | await asyncio.sleep(1) 27 | arb.stop() 28 | 29 | 30 | if __name__ == '__main__': 31 | cfg = Config() 32 | cfg.parse_command_line() 33 | arbiter(cfg=cfg, start=Greeter()).start() 34 | -------------------------------------------------------------------------------- /examples/snippets/hello.py: -------------------------------------------------------------------------------- 1 | '''Write Hello there! every second 2 | ''' 3 | from pulsar.api import arbiter 4 | 5 | 6 | def hello(actor, **kw): 7 | print('Hello there!') 8 | actor._loop.call_later(1, hello, actor) 9 | 10 | 11 | if __name__ == '__main__': 12 | arbiter(start=hello).start() 13 | -------------------------------------------------------------------------------- /examples/snippets/tunnel.py: -------------------------------------------------------------------------------- 1 | from pulsar.apps import http 2 | import requests # noqa 3 | 4 | if __name__ == '__main__': 5 | session = http.HttpClient() 6 | # session = requests.session() 7 | # 8 | url = 'https://api.github.com/' 9 | verify = True 10 | # 11 | url = 'https://127.0.0.1:8070' 12 | verify = False 13 | # 14 | proxies = {'https': 'http://127.0.0.1:8060'} 15 | response = session.get(url, proxies=proxies, verify=verify) 16 | print(response) 17 | # proxies = {'https': 'http://127.0.0.1:8080'} 18 | # response = session.get(url, proxies=proxies, verify=verify) 19 | # print(response) 20 | proxies = {'https': 'http://127.0.0.1:8060'} 21 | response = session.get(url, proxies=proxies, verify=verify) 22 | print(response) 23 | -------------------------------------------------------------------------------- /examples/websocket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/examples/websocket/__init__.py -------------------------------------------------------------------------------- /examples/websocket/manage.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A very Simple Web-Socket example. 3 | To run the server type:: 4 | 5 | python manage.py 6 | 7 | and open a web browser at http://localhost:8060 8 | ''' 9 | import os 10 | from random import random 11 | 12 | from pulsar.apps import ws, wsgi 13 | from pulsar.utils.system import json 14 | 15 | 16 | DIR = os.path.dirname(__file__) 17 | 18 | 19 | class Graph(ws.WS): 20 | 21 | def on_message(self, websocket, msg): 22 | websocket.write(json.dumps([(i, random()) for i in range(100)])) 23 | 24 | 25 | class Echo(ws.WS): 26 | 27 | def on_message(self, websocket, msg): 28 | if msg.startswith('send ping '): 29 | websocket.ping(msg[10:]) 30 | elif msg.startswith('send close '): 31 | websocket.write_close(int(msg[11:])) 32 | else: 33 | websocket.write(msg) 34 | 35 | 36 | class Site(wsgi.LazyWsgi): 37 | 38 | def __init__(self, pyparser=False): 39 | self.pyparser = pyparser 40 | 41 | def setup(self, environ): 42 | return wsgi.WsgiHandler( 43 | [wsgi.Router('/', get=self.home), 44 | ws.WebSocket('/data', Graph()), 45 | ws.WebSocket('/echo', Echo())]) 46 | 47 | def home(self, request): 48 | response = request.response 49 | response.content_type = 'text/html' 50 | response.encoding = 'utf-8' 51 | with open(os.path.join(DIR, 'websocket.html')) as f: 52 | response.content = f.read() % request.environ 53 | return response 54 | 55 | 56 | def server(pyparser=False, **kwargs): 57 | return wsgi.WSGIServer(callable=Site(pyparser), **kwargs) 58 | 59 | 60 | if __name__ == '__main__': # pragma nocover 61 | server().start() 62 | -------------------------------------------------------------------------------- /examples/websocket/websocket.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Websocket example in pulsar 6 | 7 | 8 | 57 | 58 | 59 |

Plot

60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/extensions/__init__.py -------------------------------------------------------------------------------- /extensions/ext.py: -------------------------------------------------------------------------------- 1 | # 2 | # Required by Cython to build Hiredis extensions 3 | # 4 | import os 5 | import sys 6 | from distutils.extension import Extension 7 | from distutils.command.build_ext import build_ext 8 | from distutils.errors import (CCompilerError, DistutilsExecError, 9 | DistutilsPlatformError) 10 | 11 | path = os.path.join('extensions', 'lib') 12 | ext_file = os.path.join(path, 'clib.c') 13 | ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError) 14 | 15 | if sys.platform == 'win32': 16 | # 2.6's distutils.msvc9compiler can raise an IOError when failing to 17 | # find the compiler 18 | ext_errors += (IOError,) 19 | 20 | 21 | class BuildFailed(Exception): 22 | 23 | def __init__(self, msg=None): 24 | if not msg: 25 | msg = str(sys.exc_info()[1]) 26 | self.msg = msg 27 | 28 | 29 | class tolerant_build_ext(build_ext): 30 | # This class allows C extension building to fail. From SQLAlchemy 31 | 32 | def run(self): 33 | try: 34 | if not os.path.isfile(ext_file): 35 | try: 36 | from Cython.Build import cythonize 37 | except ImportError: 38 | raise BuildFailed('Cython not installed') 39 | self.extensions = cythonize(self.extensions, 40 | include_path=[path]) 41 | super().run() 42 | except DistutilsPlatformError: 43 | raise BuildFailed 44 | 45 | def build_extension(self, ext): 46 | try: 47 | super().build_extension(ext) 48 | except ext_errors: 49 | raise BuildFailed 50 | except ValueError: 51 | # this can happen on Windows 64 bit, see Python issue 7511 52 | if "'path'" in str(sys.exc_info()[1]): # works with both py 2/3 53 | raise BuildFailed 54 | raise 55 | 56 | 57 | def params(cython=False): 58 | if not cython: 59 | cython = not os.path.isfile(ext_file) 60 | 61 | if cython and os.path.isfile(ext_file): 62 | os.remove(ext_file) 63 | 64 | file_name = 'clib.pyx' if cython else 'clib.c' 65 | 66 | extension = Extension('pulsar.utils.clib', 67 | [os.path.join(path, file_name)], 68 | include_dirs=[path]) 69 | 70 | extensions = [extension] 71 | 72 | return {'ext_modules': extensions, 73 | 'cmdclass': {'build_ext': tolerant_build_ext}, 74 | 'include_dirs': [path]} 75 | -------------------------------------------------------------------------------- /extensions/lib/clib.pxd: -------------------------------------------------------------------------------- 1 | 2 | cdef extern from "math.h": 3 | double log(double x) 4 | double sqrt(double x) 5 | 6 | cdef double clog2 = log(2.) 7 | 8 | cdef inline int int_max(int a, int b): 9 | return a if a >= b else b 10 | 11 | 12 | cdef inline int int_min(int a, int b): 13 | return a if a >= b else b 14 | 15 | 16 | # MSVC does not have log2! 17 | cdef inline double Log2(double x): 18 | return log(x) / clog2 19 | 20 | 21 | cdef class Event: 22 | cdef readonly str name 23 | cdef int _onetime 24 | cdef list _handlers 25 | cdef object _self 26 | cdef object _waiter 27 | cpdef object onetime(self) 28 | cpdef object fired(self) 29 | cpdef list handlers(self) 30 | cpdef bind(self, object callback) 31 | cpdef clear(self) 32 | cpdef int unbind(self, object callback) 33 | cpdef fire(self, exc=?, data=?) 34 | cpdef object waiter(self) 35 | 36 | 37 | cdef class EventHandler: 38 | cdef dict _events 39 | cpdef dict events(self) 40 | cpdef Event event(self, str name) 41 | cpdef copy_many_times_events(self, EventHandler other) 42 | cpdef bind_events(self, dict events) 43 | cpdef fire_event(self, str name, exc=?, data=?) 44 | -------------------------------------------------------------------------------- /extensions/lib/clib.pyx: -------------------------------------------------------------------------------- 1 | include "globals.pyx" 2 | include "rparser.pyx" 3 | include "websocket.pyx" 4 | include "events.pyx" 5 | include "protocols.pyx" 6 | include "wsgi.pyx" 7 | include "wsgiresponse.pyx" 8 | include "utils.pyx" 9 | -------------------------------------------------------------------------------- /extensions/lib/globals.pyx: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from multidict import istr 4 | 5 | 6 | cdef bytes CRLF = b'\r\n' 7 | cdef str CHARSET = 'ISO-8859-1' 8 | cdef DEFAULT_HTTP = 'HTTP/1.1' 9 | 10 | cdef str URL_SCHEME = os.environ.get('wsgi.url_scheme', 'http') 11 | cdef str OS_SCRIPT_NAME = os.environ.get("SCRIPT_NAME", "") 12 | cdef str PULSAR_CACHE = 'pulsar.cache' 13 | cdef int MAX_CHUNK_SIZE = 65536 14 | cdef object TLS_SCHEMES = frozenset(('https', 'wss')) 15 | cdef object NO_CONTENT_CODES = frozenset((204, 304)) 16 | cdef object NO_BODY_VERBS = frozenset(('HEAD',)) 17 | 18 | cdef object COOKIE = istr('Cookie') 19 | cdef object CONNECTION = istr('Connection') 20 | cdef object CONTENT_LENGTH = istr('Content-Length') 21 | cdef object CONTENT_TYPE = istr('Content-Type') 22 | cdef object DATE = istr('Date') 23 | cdef object EXPECT = istr('Expect') 24 | cdef object HOST = istr('Host') 25 | cdef object KEEP_ALIVE = istr('Keep-Alive') 26 | cdef object LOCATION = istr('Location') 27 | cdef object PROXY_AUTHENTICATE = istr('Proxy-Authenticate') 28 | cdef object PROXY_AUTHORIZATION = istr('Proxy-Authorization') 29 | cdef object SCRIPT_NAME = istr("Script_Name") 30 | cdef object SERVER = istr('Server') 31 | cdef object SET_COOKIE = istr('Set-Cookie') 32 | cdef object TE = istr('Te') 33 | cdef object TRAILERS = istr('Trailers') 34 | cdef object TRANSFER_ENCODING = istr('Transfer-Encoding') 35 | cdef object UPGRADE = istr('Upgrade') 36 | cdef object X_FORWARDED_FOR = istr('X-Forwarded-For') 37 | cdef object X_FORWARDED_PROTOCOL = istr("X-Forwarded-Protocol") 38 | cdef object X_FORWARDED_PROTO = istr("X-Forwarded-Proto") 39 | cdef object X_FORWARDED_SSL = istr("X-Forwarded-Ssl") 40 | 41 | cdef object HOP_HEADERS = frozenset(( 42 | CONNECTION, KEEP_ALIVE, PROXY_AUTHENTICATE, 43 | PROXY_AUTHORIZATION, TE, TRAILERS, 44 | TRANSFER_ENCODING, UPGRADE 45 | )) 46 | cdef str _http_date_ = '' 47 | cdef int _http_time_ = 0 48 | -------------------------------------------------------------------------------- /extensions/lib/http.h: -------------------------------------------------------------------------------- 1 | #ifndef __PULSAR_HTTP__ 2 | #define __PULSAR_HTTP__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | static char *week[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; 10 | static char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", 11 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; 12 | 13 | static PyObject* _http_date(time_t timestamp) { 14 | char buf[50]; 15 | struct tm gmt = *gmtime(×tamp); 16 | //strftime(buf, sizeof buf, "%a, %d %b %Y %H:%M:%S GMT", &gmt); 17 | sprintf(buf, "%s, %02d %s %4d %02d:%02d:%02d GMT", 18 | week[gmt.tm_wday], gmt.tm_mday, 19 | months[gmt.tm_mon], gmt.tm_year + 1900, 20 | gmt.tm_hour, gmt.tm_min, gmt.tm_sec); 21 | return PyUnicode_FromString(&buf); 22 | } 23 | 24 | 25 | #endif // __PULSAR_HTTP__ 26 | -------------------------------------------------------------------------------- /extensions/lib/utils.pyx: -------------------------------------------------------------------------------- 1 | from types import CoroutineType 2 | from collections.abc import Awaitable 3 | 4 | 5 | cdef tuple AWAITABLES = (CoroutineType, Awaitable) 6 | 7 | 8 | cpdef object isawaitable(object object): 9 | """Return true if object can be passed to an ``await`` expression.""" 10 | return isinstance(object, AWAITABLES) 11 | -------------------------------------------------------------------------------- /extensions/lib/websocket.h: -------------------------------------------------------------------------------- 1 | #ifndef __PULSAR_WEBSOCKET__ 2 | #define __PULSAR_WEBSOCKET__ 3 | 4 | #include 5 | 6 | 7 | PyObject* websocket_mask(const char* chunk, const char* key, 8 | size_t chunk_length, size_t mask_length) { 9 | size_t i; 10 | char* buf; 11 | PyObject* result = PyBytes_FromStringAndSize(NULL, chunk_length); 12 | if (!result) { 13 | return NULL; 14 | } 15 | buf = PyBytes_AS_STRING(result); 16 | for (i = 0; i < chunk_length; i++) { 17 | buf[i] = chunk[i] ^ key[i % mask_length]; 18 | } 19 | return result; 20 | } 21 | 22 | 23 | #endif // __PULSAR_WEBSOCKET__ 24 | -------------------------------------------------------------------------------- /extensions/lib/websocket.pxd: -------------------------------------------------------------------------------- 1 | 2 | cdef extern from "websocket.h": 3 | object websocket_mask(const char* chunk, const char* key, 4 | size_t chunk_length, size_t mask_length) 5 | 6 | 7 | cdef inline bytes to_bytes(object value, str encoding): 8 | if isinstance(value, bytes): 9 | return value 10 | elif isinstance(value, str): 11 | return value.encode(encoding) 12 | else: 13 | raise TypeError('Requires bytes or string') 14 | -------------------------------------------------------------------------------- /extensions/lib/wsgi.pxd: -------------------------------------------------------------------------------- 1 | 2 | cdef extern from "http.h": 3 | ctypedef int time_t 4 | object _http_date(time_t timestamp) 5 | -------------------------------------------------------------------------------- /install-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pip install --upgrade pip wheel 4 | pip install --upgrade setuptools 5 | pip install -r requirements/dev.txt 6 | pip install -r requirements/hard.txt 7 | pyslink pulsar 8 | -------------------------------------------------------------------------------- /pulsar/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | """Event driven concurrent framework for Python""" 3 | from .utils.version import get_version 4 | 5 | 6 | VERSION = (2, 0, 2, 'final', 0) 7 | 8 | 9 | __version__ = version = get_version(VERSION) 10 | __author__ = "Luca Sbardella" 11 | 12 | 13 | DEFAULT_PORT = 8060 14 | ASYNC_TIMEOUT = None 15 | SERVER_NAME = 'pulsar' 16 | JAPANESE = b'\xe3\x83\x91\xe3\x83\xab\xe3\x82\xb5\xe3\x83\xbc'.decode('utf-8') 17 | CHINESE = b'\xe8\x84\x89\xe5\x86\xb2\xe6\x98\x9f'.decode('utf-8') 18 | HINDI = (b'\xe0\xa4\xaa\xe0\xa4\xb2\xe0\xa5\x8d' 19 | b'\xe0\xa4\xb8\xe0\xa4\xb0').decode('utf-8') 20 | SERVER_SOFTWARE = "{0}/{1}".format(SERVER_NAME, version) 21 | -------------------------------------------------------------------------------- /pulsar/apps/data/__init__.py: -------------------------------------------------------------------------------- 1 | from .store import ( 2 | Command, Store, RemoteStore, PubSub, PubSubClient, 3 | parse_store_url, create_store, register_store, data_stores, 4 | NoSuchStore 5 | ) 6 | from .channels import Channels 7 | from . import redis # noqa 8 | from .pulsards.startds import start_store 9 | 10 | 11 | __all__ = [ 12 | 'Command', 13 | 'Store', 14 | 'RemoteStore', 15 | 'PubSub', 16 | 'PubSubClient', 17 | 'parse_store_url', 18 | 'create_store', 19 | 'register_store', 20 | 'data_stores', 21 | 'NoSuchStore', 22 | 'start_store', 23 | 'Channels' 24 | ] 25 | -------------------------------------------------------------------------------- /pulsar/apps/data/pulsards/__init__.py: -------------------------------------------------------------------------------- 1 | from pulsar.apps.data import register_store 2 | 3 | from ..redis import store 4 | 5 | 6 | class PulsarStore(store.RedisStore): 7 | pass 8 | 9 | 10 | register_store('pulsar', 'pulsar.apps.data.pulsards:PulsarStore') 11 | -------------------------------------------------------------------------------- /pulsar/apps/data/pulsards/startds.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from ....async.actor import send 4 | from ... import when_monitor_start, get_application 5 | from ...ds import PulsarDS 6 | from ..store import create_store 7 | 8 | 9 | async def start_pulsar_ds(arbiter, host, workers=0): 10 | lock = getattr(arbiter, 'lock', None) 11 | if lock is None: 12 | arbiter.lock = lock = asyncio.Lock() 13 | await lock.acquire() 14 | try: 15 | app = await get_application('pulsards') 16 | if not app: 17 | app = PulsarDS(bind=host, workers=workers, load_config=False) 18 | cfg = await app(arbiter) 19 | else: 20 | cfg = app.cfg 21 | return cfg 22 | finally: 23 | lock.release() 24 | 25 | 26 | async def start_store(app, url, workers=0, **kw): 27 | '''Equivalent to :func:`.create_store` for most cases excepts when the 28 | ``url`` is for a pulsar store not yet started. 29 | In this case, a :class:`.PulsarDS` is started. 30 | ''' 31 | store = create_store(url, **kw) 32 | if store.name == 'pulsar': 33 | client = store.client() 34 | try: 35 | await client.ping() 36 | except ConnectionRefusedError: 37 | host = localhost(store._host) 38 | if not host: 39 | raise 40 | cfg = await send('arbiter', 'run', start_pulsar_ds, 41 | host, workers) 42 | store._host = cfg.addresses[0] 43 | dsn = store.buildurl() 44 | store = create_store(dsn, **kw) 45 | app.cfg.set('data_store', store.dsn) 46 | 47 | 48 | def localhost(host): 49 | if isinstance(host, tuple): 50 | if host[0] in ('127.0.0.1', ''): 51 | return ':'.join((str(b) for b in host)) 52 | else: 53 | return host 54 | 55 | 56 | def _start_store(monitor): 57 | app = monitor.app 58 | if not isinstance(app, PulsarDS) and app.cfg.data_store: 59 | return start_store(app, app.cfg.data_store) 60 | 61 | 62 | when_monitor_start.append(_start_store) 63 | -------------------------------------------------------------------------------- /pulsar/apps/data/redis/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Pulsar is shipped with a :class:`.Store` implementation for redis_ 3 | and :ref:`pulsard-ds ` servers. 4 | 5 | .. _redis: http://redis.io/ 6 | 7 | Redis Store 8 | ~~~~~~~~~~~~ 9 | 10 | .. autoclass:: pulsar.apps.data.redis.store.RedisStore 11 | :members: 12 | :member-order: bysource 13 | 14 | 15 | Redis Client 16 | ~~~~~~~~~~~~~~~ 17 | 18 | .. autoclass:: pulsar.apps.data.redis.client.RedisClient 19 | :members: 20 | :member-order: bysource 21 | 22 | Redis Pipeline 23 | ~~~~~~~~~~~~~~~ 24 | 25 | .. autoclass:: pulsar.apps.data.redis.client.Pipeline 26 | :members: 27 | :member-order: bysource 28 | ''' 29 | from ....utils.config import Global 30 | from ..store import register_store 31 | from ...ds import RedisError, NoScriptError, redis_parser 32 | 33 | from .store import RedisStore, RedisStoreConnection 34 | from .client import ResponseError, Consumer, Pipeline 35 | from .lock import RedisScript, LockError 36 | 37 | 38 | __all__ = ['RedisStore', 'RedisError', 'NoScriptError', 'redis_parser', 39 | 'RedisStoreConnection', 'Consumer', 'Pipeline', 'ResponseError', 40 | 'RedisScript', 'LockError'] 41 | 42 | 43 | class RedisServer(Global): 44 | name = 'redis_server' 45 | flags = ['--redis-server'] 46 | meta = "CONNECTION_STRING" 47 | default = '127.0.0.1:6379/7' 48 | desc = 'Default connection string for the redis server' 49 | 50 | 51 | register_store('redis', 'pulsar.apps.data.redis:RedisStore') 52 | -------------------------------------------------------------------------------- /pulsar/apps/ds/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import PulsarDS, DEFAULT_PULSAR_STORE_ADDRESS, pulsards_url 2 | from .client import COMMANDS_INFO, redis_to_py_pattern 3 | from .parser import (RedisParser, redis_parser, 4 | RedisError, ResponseError, 5 | InvalidResponse, NoScriptError, CommandError) 6 | 7 | 8 | __all__ = ['PulsarDS', 'DEFAULT_PULSAR_STORE_ADDRESS', 'pulsards_url', 9 | 'COMMANDS_INFO', 'redis_to_py_pattern', 10 | 'RedisParser', 'redis_parser', 11 | 'RedisError', 'ResponseError', 12 | 'InvalidResponse', 'NoScriptError', 'CommandError'] 13 | -------------------------------------------------------------------------------- /pulsar/apps/ds/parser.py: -------------------------------------------------------------------------------- 1 | from ...utils.exceptions import PulsarException 2 | from ...utils.lib import RedisParser 3 | 4 | 5 | class RedisError(PulsarException): 6 | '''Redis Error Base class''' 7 | pass 8 | 9 | 10 | class CommandError(RedisError): 11 | pass 12 | 13 | 14 | class ResponseError(RedisError): 15 | pass 16 | 17 | 18 | class InvalidResponse(RedisError): 19 | pass 20 | 21 | 22 | class NoScriptError(ResponseError): 23 | pass 24 | 25 | 26 | EXCEPTION_CLASSES = { 27 | 'ERR': ResponseError, 28 | 'NOSCRIPT': NoScriptError, 29 | } 30 | 31 | 32 | def response_error(response): 33 | "Parse an error response" 34 | response = response.split(' ') 35 | error_code = response[0] 36 | if error_code not in EXCEPTION_CLASSES: 37 | error_code = 'ERR' 38 | response = ' '.join(response[1:]) 39 | return EXCEPTION_CLASSES[error_code](response) 40 | 41 | 42 | def redis_parser(): 43 | return RedisParser(InvalidResponse, response_error) 44 | -------------------------------------------------------------------------------- /pulsar/apps/greenio/__init__.py: -------------------------------------------------------------------------------- 1 | from greenlet import greenlet, getcurrent 2 | 3 | from .pool import GreenPool 4 | from .lock import GreenLock 5 | from .http import GreenHttp 6 | from .wsgi import GreenWSGI 7 | from .utils import MustBeInChildGreenlet, wait, run_in_greenlet 8 | 9 | __all__ = ['GreenPool', 10 | 'GreenLock', 11 | 'GreenHttp', 12 | 'GreenWSGI', 13 | 'MustBeInChildGreenlet', 14 | 'wait', 15 | 'run_in_greenlet', 16 | 'greenlet', 17 | 'getcurrent'] 18 | -------------------------------------------------------------------------------- /pulsar/apps/greenio/http.py: -------------------------------------------------------------------------------- 1 | from pulsar.apps.http import HttpClient 2 | 3 | from .utils import wait 4 | 5 | 6 | class GreenHttp: 7 | """Wraps an http client so we can use it with greenlets 8 | """ 9 | def __init__(self, http=None): 10 | self._http = http or HttpClient() 11 | 12 | def __getattr__(self, name): 13 | return getattr(self._http, name) 14 | 15 | def get(self, url, **kwargs): 16 | return self.request('GET', url, **kwargs) 17 | 18 | def options(self, url, **kwargs): 19 | return self.request('OPTIONS', url, **kwargs) 20 | 21 | def head(self, url, **kwargs): 22 | return self.request('HEAD', url, **kwargs) 23 | 24 | def post(self, url, **kwargs): 25 | return self.request('POST', url, **kwargs) 26 | 27 | def put(self, url, **kwargs): 28 | return self.request('PUT', url, **kwargs) 29 | 30 | def patch(self, url, **kwargs): 31 | return self.request('PATCH', url, **kwargs) 32 | 33 | def delete(self, url, **kwargs): 34 | return self.request('DELETE', url, **kwargs) 35 | 36 | def request(self, method, url, **kw): 37 | return wait(self._http.request(method, url, **kw), True) 38 | -------------------------------------------------------------------------------- /pulsar/apps/greenio/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from functools import wraps 3 | from inspect import isawaitable 4 | 5 | from greenlet import greenlet, getcurrent 6 | 7 | 8 | class MustBeInChildGreenlet(RuntimeError): 9 | """Raised when an operation must be performed in a child greenlet 10 | """ 11 | 12 | 13 | class GreenletWorker(greenlet): 14 | pass 15 | 16 | 17 | def wait(value, must_be_child=False): 18 | '''Wait for a possible asynchronous value to complete. 19 | ''' 20 | current = getcurrent() 21 | parent = current.parent 22 | if must_be_child and not parent: 23 | raise MustBeInChildGreenlet('Cannot wait on main greenlet') 24 | return parent.switch(value) if parent else value 25 | 26 | 27 | def run_in_greenlet(callable): 28 | """Decorator to run a ``callable`` on a new greenlet. 29 | 30 | A ``callable`` decorated with this decorator returns a coroutine 31 | """ 32 | @wraps(callable) 33 | async def _(*args, **kwargs): 34 | green = greenlet(callable) 35 | # switch to the new greenlet 36 | result = green.switch(*args, **kwargs) 37 | # back to the parent 38 | while isawaitable(result): 39 | # keep on switching back to the greenlet if we get an awaitable 40 | try: 41 | result = green.switch((await result)) 42 | except Exception: 43 | exc_info = sys.exc_info() 44 | result = green.throw(*exc_info) 45 | 46 | return green.switch(result) 47 | 48 | return _ 49 | -------------------------------------------------------------------------------- /pulsar/apps/greenio/wsgi.py: -------------------------------------------------------------------------------- 1 | from pulsar.api import Http404 2 | from pulsar.apps.wsgi import handle_wsgi_error 3 | 4 | from .utils import wait 5 | 6 | 7 | class GreenWSGI: 8 | '''Wraps a WSGI application to be executed on a :class:`.GreenPool` 9 | ''' 10 | def __init__(self, middleware, pool, response_middleware=None): 11 | if not isinstance(middleware, (list, tuple)): 12 | middleware = [middleware] 13 | self.middleware = list(middleware) 14 | self.response_middleware = response_middleware or [] 15 | self.pool = pool 16 | 17 | def __call__(self, environ, start_response): 18 | if self.pool.in_green_worker: 19 | return self._call(environ, start_response) 20 | else: 21 | return self.pool.submit(self._call, environ, start_response) 22 | 23 | def _call(self, environ, start_response): 24 | wsgi_input = environ['wsgi.input'] 25 | if wsgi_input and not isinstance(wsgi_input, GreenStream): 26 | environ['wsgi.input'] = GreenStream(wsgi_input) 27 | response = None 28 | try: 29 | for middleware in self.middleware: 30 | response = wait(middleware(environ, start_response)) 31 | if response is not None: 32 | break 33 | if response is None: 34 | raise Http404 35 | 36 | except Exception as exc: 37 | response = wait(handle_wsgi_error(environ, exc)) 38 | 39 | if not getattr(response, '__wsgi_started__', True): 40 | for middleware in self.response_middleware: 41 | response = wait(middleware(environ, response)) or response 42 | response.start(environ, start_response) 43 | return response 44 | 45 | 46 | class GreenStream: 47 | __slots__ = ('stream',) 48 | 49 | def __init__(self, stream): 50 | self.stream = stream 51 | 52 | def __getattr__(self, name): 53 | value = getattr(self.stream, name) 54 | if getattr(value, '__self__', None) is self.stream: 55 | return _green(value) 56 | return value 57 | 58 | 59 | class _green: 60 | __slots__ = ('value',) 61 | 62 | def __init__(self, value): 63 | self.value = value 64 | 65 | def __getattr__(self, name): 66 | return getattr(self.value, name) 67 | 68 | def __call__(self, *args, **kwargs): 69 | return wait(self.value(*args, **kwargs)) 70 | -------------------------------------------------------------------------------- /pulsar/apps/http/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import ( 2 | HttpRequest, HttpResponse, HttpClient, HttpRequestException, SSLError, 3 | full_url, FORM_URL_ENCODED 4 | ) 5 | from .wsgi import HttpWsgiClient 6 | from .plugins import TooManyRedirects 7 | from .auth import Auth, HTTPBasicAuth, HTTPDigestAuth 8 | from .oauth import OAuth1, OAuth2 9 | from .stream import HttpStream, StreamConsumedError 10 | 11 | 12 | __all__ = [ 13 | 'HttpRequest', 14 | 'HttpResponse', 15 | 'HttpClient', 16 | 'HttpWsgiClient', 17 | # 18 | 'HttpRequestException', 19 | 'TooManyRedirects', 20 | 'SSLError', 21 | # 22 | 'Auth', 23 | 'HTTPBasicAuth', 24 | 'HTTPDigestAuth', 25 | 'OAuth1', 26 | 'OAuth2', 27 | # 28 | 'HttpStream', 29 | 'StreamConsumedError', 30 | # 31 | 'full_url', 32 | 'FORM_URL_ENCODED' 33 | ] 34 | -------------------------------------------------------------------------------- /pulsar/apps/http/decompress.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | 3 | 4 | class GzipDecompress: 5 | 6 | def __call__(self, data): 7 | deco = zlib.decompressobj(16 + zlib.MAX_WBITS) 8 | return deco.decompress(data) 9 | 10 | 11 | class DeflateDecompress: 12 | 13 | def __call__(self, data): 14 | try: 15 | return zlib.decompressobj().decompress(data) 16 | except zlib.error: 17 | return zlib.decompressobj(-zlib.MAX_WBITS).decompress(data) 18 | -------------------------------------------------------------------------------- /pulsar/apps/http/oauth.py: -------------------------------------------------------------------------------- 1 | try: 2 | from oauthlib import oauth1, oauth2 3 | except ImportError: # pragma nocover 4 | oauth1 = None 5 | oauth2 = None 6 | 7 | from pulsar.api import ImproperlyConfigured 8 | from pulsar.utils.structures import mapping_iterator 9 | 10 | from . import auth 11 | 12 | 13 | class OAuth1(auth.Auth): 14 | '''Add OAuth1 authentication to pulsar :class:`.HttpClient` 15 | ''' 16 | available = bool(oauth1) 17 | 18 | def __init__(self, client_id=None, client=None, **kw): 19 | if oauth1 is None: # pragma nocover 20 | raise ImproperlyConfigured('%s requires oauthlib' % 21 | self.__class__.__name__) 22 | self._client = client or oauth1.Client(client_id, **kw) 23 | 24 | def __call__(self, response, **kw): 25 | r = response.request 26 | url, headers, _ = self._client.sign( 27 | r.url, r.method, r.body, r.headers) 28 | for key, value in mapping_iterator(headers): 29 | r.add_header(key, value) 30 | r.url = url 31 | 32 | 33 | class OAuth2(auth.Auth): 34 | """Add OAuth2 authentication to pulsar :class:`.HttpClient` 35 | """ 36 | available = bool(oauth2) 37 | 38 | def __init__(self, client_id=None, client=None, **kw): 39 | if oauth2 is None: # pragma nocover 40 | raise ImproperlyConfigured('%s requires oauthlib' % 41 | self.__class__.__name__) 42 | self.client = client or oauth2.WebApplicationClient(client_id, **kw) 43 | 44 | def __call__(self, response, **kw): 45 | r = response.request 46 | url, headers, _ = self.client.add_token( 47 | r.url, http_method=r.method, body=r.body, headers=r.headers) 48 | assert r.headers == headers 49 | # for key, value in mapping_iterator(headers): 50 | # r.add_header(key, value) 51 | r.url = url 52 | -------------------------------------------------------------------------------- /pulsar/apps/http/stream.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | class StreamConsumedError(Exception): 5 | """Same name as request error""" 6 | pass 7 | 8 | 9 | class HttpStream: 10 | """An asynchronous streaming body for an HTTP response 11 | """ 12 | def __init__(self, response): 13 | self._response = response 14 | self._streamed = False 15 | self._queue = asyncio.Queue() 16 | 17 | def __repr__(self): 18 | return repr(self._response) 19 | __str__ = __repr__ 20 | 21 | @property 22 | def done(self): 23 | """Check if the stream is finished 24 | """ 25 | return self._response.event('post_request').fired() 26 | 27 | async def read(self, n=None): 28 | """Read all content 29 | """ 30 | if self._streamed: 31 | return b'' 32 | buffer = [] 33 | async for body in self: 34 | buffer.append(body) 35 | return b''.join(buffer) 36 | 37 | def close(self): 38 | pass 39 | 40 | def __iter__(self): 41 | return _start_iter(self) 42 | 43 | def __next__(self): 44 | if self.done: 45 | try: 46 | return self._queue.get_nowait() 47 | except asyncio.QueueEmpty: 48 | raise StopIteration 49 | else: 50 | return self._queue.get() 51 | 52 | async def __aiter__(self): 53 | return _start_iter(self) 54 | 55 | async def __anext__(self): 56 | if self.done: 57 | try: 58 | return self._queue.get_nowait() 59 | except asyncio.QueueEmpty: 60 | raise StopAsyncIteration 61 | else: 62 | return await self._queue.get() 63 | 64 | def feed_data(self, body): 65 | self._queue.put_nowait(body) 66 | 67 | 68 | def _start_iter(self): 69 | if self._streamed: 70 | raise StreamConsumedError 71 | self._streamed = True 72 | return self 73 | -------------------------------------------------------------------------------- /pulsar/apps/rpc/__init__.py: -------------------------------------------------------------------------------- 1 | """Asynchronous WSGI_ Remote Procedure Calls middleware. It implements a 2 | JSON-RPC_ server and client. Check out the 3 | :ref:`json-rpc tutorial ` if you want to get started 4 | quickly with a working example. 5 | 6 | To quickly setup a server:: 7 | 8 | class MyRpc(rpc.JSONRPC): 9 | 10 | def rpc_ping(self, request): 11 | return 'pong' 12 | 13 | 14 | class Wsgi(wsgi.LazyWsgi): 15 | 16 | def handler(self, environ=None): 17 | app = wsgi.Router('/', 18 | post=MyRpc(), 19 | response_content_types=['application/json']) 20 | return wsgi.WsgiHandler([app]) 21 | 22 | if __name__ == '__main__': 23 | wsgi.WSGIServer(Wsgi()).start() 24 | 25 | 26 | * The ``MyRpc`` handles the requests 27 | * Routing is delegated to the :class:`.Router` which handle only ``post`` 28 | requests with content type ``application/json``. 29 | 30 | 31 | API 32 | =========== 33 | 34 | .. module:: pulsar.apps.rpc.handlers 35 | 36 | RpcHandler 37 | ~~~~~~~~~~~~~~ 38 | 39 | .. autoclass:: RpcHandler 40 | :members: 41 | :member-order: bysource 42 | 43 | rpc method decorator 44 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 45 | 46 | .. autofunction:: rpc_method 47 | 48 | 49 | .. module:: pulsar.apps.rpc.jsonrpc 50 | 51 | JSON RPC 52 | ~~~~~~~~~~~~~~~~ 53 | 54 | .. autoclass:: JSONRPC 55 | :members: 56 | :member-order: bysource 57 | 58 | 59 | JsonProxy 60 | ~~~~~~~~~~~~~~~~ 61 | 62 | .. autoclass:: JsonProxy 63 | :members: 64 | :member-order: bysource 65 | 66 | 67 | .. module:: pulsar.apps.rpc.mixins 68 | 69 | Server Commands 70 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 71 | 72 | .. autoclass:: PulsarServerCommands 73 | :members: 74 | :member-order: bysource 75 | 76 | .. _JSON-RPC: http://www.jsonrpc.org/specification 77 | .. _WSGI: http://www.python.org/dev/peps/pep-3333/ 78 | """ 79 | from .handlers import ( 80 | RpcHandler, rpc_method, InvalidRequest, InvalidParams, 81 | NoSuchFunction, InternalError 82 | ) 83 | from .jsonrpc import JSONRPC, JsonProxy, JsonBatchProxy 84 | from .mixins import PulsarServerCommands 85 | 86 | 87 | __all__ = [ 88 | 'RpcHandler', 89 | 'rpc_method', 90 | 'InvalidRequest', 91 | 'InvalidParams', 92 | 'NoSuchFunction', 93 | 'InternalError', 94 | 'JSONRPC', 95 | 'JsonProxy', 96 | 'JsonBatchProxy', 97 | 'PulsarServerCommands' 98 | ] 99 | -------------------------------------------------------------------------------- /pulsar/apps/rpc/mixins.py: -------------------------------------------------------------------------------- 1 | from pulsar.api import send 2 | 3 | from .jsonrpc import JSONRPC 4 | 5 | 6 | class PulsarServerCommands(JSONRPC): 7 | '''Useful commands to add to your :class:`.JSONRPC` handler. 8 | 9 | It exposes the following functions:''' 10 | def rpc_ping(self, request): 11 | '''Ping the server.''' 12 | return 'pong' 13 | 14 | def rpc_echo(self, request, message=''): 15 | '''Echo the server.''' 16 | return message 17 | 18 | async def rpc_server_info(self, request): 19 | '''Return a dictionary of information regarding the server and workers. 20 | 21 | It invokes the :meth:`extra_server_info` for adding custom 22 | information. 23 | ''' 24 | info = await send('arbiter', 'info') 25 | info = self.extra_server_info(request, info) 26 | try: 27 | info = await info 28 | except TypeError: 29 | pass 30 | return info 31 | 32 | def rpc_functions_list(self, request): 33 | '''List of (method name, method document) pair of all method exposed 34 | by this :class:`.JSONRPC` handler.''' 35 | return list(self.listFunctions()) 36 | 37 | def rpc_documentation(self, request): 38 | '''Documentation in restructured text.''' 39 | return self.docs() 40 | 41 | def rpc_kill_actor(self, request, aid): 42 | '''Kill the actor with id equal to *aid*.''' 43 | return send('arbiter', 'kill_actor', aid) 44 | 45 | def extra_server_info(self, request, info): 46 | '''An internal method. 47 | 48 | Used by the :meth:`rpc_server_info` method to add additional 49 | information to the info dictionary. 50 | ''' 51 | return info 52 | -------------------------------------------------------------------------------- /pulsar/apps/test/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/pulsar/apps/test/plugins/__init__.py -------------------------------------------------------------------------------- /pulsar/apps/test/plugins/htmlfiles/profile/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Profile report 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 |
22 |
23 | 24 | {0[table]} 25 |
26 |
27 |
28 | 29 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /pulsar/apps/test/plugins/htmlfiles/profile/profile.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | $(document).ready(function() { 4 | $("table.tablesorter").tablesorter(); 5 | }); 6 | 7 | }(jQuery)); -------------------------------------------------------------------------------- /pulsar/apps/test/plugins/htmlfiles/profile/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: georgia, serif; 3 | font-size: 14px; 4 | color: #222222; 5 | } 6 | h1 { 7 | font-size: 200%; 8 | font-weight: bold; 9 | color: #333333; 10 | margin-bottom: 10px 11 | } 12 | p { 13 | font-size: 120%; 14 | } 15 | .container { 16 | width: 100%; 17 | float: left; 18 | } 19 | .content { 20 | padding: 10px 20px; 21 | } 22 | #footer { 23 | font-size: 85%; 24 | font-family: verdana, sans-serif; 25 | color: #666666; 26 | font-style: italic; 27 | } 28 | 29 | table { 30 | border: none; 31 | border-collapse: collapse; 32 | } 33 | table.tablesorter { 34 | border: none; 35 | table-layout: auto; 36 | font-family:arial; 37 | background-color: #fff; 38 | margin:10px 0 15px; 39 | width: 100%; 40 | text-align: left; 41 | } 42 | table.tablesorter thead tr th { 43 | padding: 5px 7px; 44 | } 45 | table.tablesorter thead tr .header { 46 | font-weight: bold; 47 | cursor: pointer; 48 | } 49 | table.tablesorter tbody td { 50 | padding: 5px 7px; 51 | vertical-align: top; 52 | } 53 | table.tablesorter tbody tr.odd td { 54 | background-color:#F0F0F6; 55 | } 56 | table.tablesorter tbody tr { 57 | border-bottom: 1px solid #d4d4d4; 58 | } 59 | table.tablesorter thead tr th { 60 | background-color: #8dbdd8; 61 | border: 1px solid #fff; 62 | } 63 | table.tablesorter thead tr th:hover { 64 | background-color: #C7DEEB; 65 | } 66 | table.tablesorter thead tr th.headerSortDown, 67 | table.tablesorter thead tr th.headerSortUp { 68 | background-color: #275672; 69 | color: #fff; 70 | } 71 | -------------------------------------------------------------------------------- /pulsar/apps/test/wsgi.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | from unittest.mock import MagicMock 3 | 4 | from pulsar.apps.wsgi import wsgi_request 5 | from pulsar.apps.http import HttpWsgiClient 6 | 7 | 8 | async def test_wsgi_request(url=None, method=None, headers=None, **kwargs): 9 | """Create a valid WSGI request 10 | 11 | :param url: optional path 12 | :param method: optional HTTP method, defaults to GET 13 | :param headers: optional list-alike collection of headers 14 | :return: :class:`~.WsgiRequest` 15 | """ 16 | cli = HttpWsgiClient(ok, headers=headers) 17 | url = url or '/' 18 | if not urlparse(url).scheme: 19 | url = 'http://www.example.com/%s' % ( 20 | url[1:] if url.startswith('/') else url) 21 | method = method or 'get' 22 | response = await cli.request(method, url, **kwargs) 23 | return wsgi_request(response.server_side.request.environ) 24 | 25 | 26 | def ok(environ, start_response): 27 | request = wsgi_request(environ) 28 | request.cache.logger = MagicMock() 29 | response_headers = [ 30 | ('Content-Length', '0') 31 | ] 32 | start_response('200 OK', response_headers) 33 | return iter([]) 34 | -------------------------------------------------------------------------------- /pulsar/apps/ws/extensions.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | 3 | from pulsar.utils import websocket 4 | 5 | ############################################################################ 6 | # x-webkit-deflate-frame Extension 7 | # 8 | # http://code.google.com/p/pywebsocket/ 9 | 10 | 11 | class deflate_frame(websocket.Extension): 12 | 13 | def __init__(self, window_bits=None): 14 | self.window_bits = window_bits or zlib.MAX_WBITS 15 | 16 | def receive(self, frame, application_data): 17 | if frame.rsv1 == 1: 18 | application_data += b'\x00\x00\xff\xff' 19 | return zlib.decompress(application_data) 20 | else: 21 | return application_data 22 | 23 | def send(self, frame, application_data): 24 | application_data = self._compress.compress(application_data) 25 | return application_data 26 | 27 | 28 | # websocket.WS_EXTENSIONS['x-webkit-deflate-frame'] = deflate_frame 29 | -------------------------------------------------------------------------------- /pulsar/apps/wsgi/html.py: -------------------------------------------------------------------------------- 1 | from collections import Mapping 2 | 3 | from pulsar.utils.structures import recursive_update 4 | 5 | HTML_VISITORS = {} 6 | 7 | 8 | newline = frozenset(('html', 'head', 'body')) 9 | 10 | 11 | def html_visitor(tag): 12 | return HTML_VISITORS.get(tag, base) 13 | 14 | 15 | class HtmlType(type): 16 | 17 | def __new__(cls, name, bases, attrs): 18 | abstract = attrs.pop('abstract', False) 19 | new_class = super().__new__(cls, name, bases, attrs) 20 | if not abstract: 21 | HTML_VISITORS[name.lower()] = new_class() 22 | return new_class 23 | 24 | 25 | class HtmlVisitor(HtmlType('HtmlVisitorBase', (object,), {'abstract': True})): 26 | '''A visitor class for :class:`Html`.''' 27 | abstract = True 28 | 29 | def get_form_value(self, html): 30 | return html.attr('value') 31 | 32 | def set_form_value(self, html, value): 33 | html.attr('value', value) 34 | 35 | def add_data(self, html, key, value): 36 | if value is not None: 37 | data = html._data 38 | if key in data and isinstance(value, Mapping): 39 | target = data[key] 40 | if isinstance(target, Mapping): 41 | return recursive_update(target, value) 42 | data[key] = value 43 | 44 | 45 | base = HtmlVisitor() 46 | 47 | 48 | class Textarea(HtmlVisitor): 49 | 50 | def get_form_value(self, html): 51 | return html.children[0] if html.children else '' 52 | 53 | def set_form_value(self, html, value): 54 | html.remove_all() 55 | html.append(value) 56 | -------------------------------------------------------------------------------- /pulsar/async/__init__.py: -------------------------------------------------------------------------------- 1 | from .concurrency import concurrency_models 2 | 3 | 4 | __all__ = ['concurrency_models'] 5 | -------------------------------------------------------------------------------- /pulsar/async/_subprocess.py: -------------------------------------------------------------------------------- 1 | 2 | if __name__ == '__main__': 3 | import sys 4 | import pickle 5 | from multiprocessing import current_process 6 | from multiprocessing.spawn import import_main_path 7 | 8 | data = pickle.load(sys.stdin.buffer) 9 | current_process().authkey = data['authkey'] 10 | sys.path = data['path'] 11 | import_main_path(data['main']) 12 | impl = pickle.loads(data['impl']) 13 | 14 | from pulsar.async.concurrency import run_actor 15 | 16 | run_actor(impl) 17 | -------------------------------------------------------------------------------- /pulsar/async/consts.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Constants used throughout pulsar. 3 | ''' 4 | from pulsar.utils.structures import AttributeDictionary 5 | 6 | # LOW LEVEL CONSTANTS - NO NEED TO CHANGE THOSE ########################### 7 | ACTOR_STATES = AttributeDictionary(INITIAL=0X0, 8 | INACTIVE=0X1, 9 | STARTING=0x2, 10 | RUN=0x3, 11 | STOPPING=0x4, 12 | CLOSE=0x5, 13 | TERMINATE=0x6) 14 | ''' 15 | .. _actor-states: 16 | 17 | Actor state constants are access via:: 18 | 19 | from pulsar.api import ACTOR_STATES 20 | 21 | They are: 22 | 23 | * ``ACTOR_STATES.INITIAL = 0`` when an actor is just created, before the 24 | :class:`pulsar.Actor.start` method is called. 25 | * ``ACTOR_STATES.STARTING = 2`` when :class:`pulsar.Actor.start` method 26 | is called. 27 | * ``ACTOR_STATES.RUN = 3`` when :class:`pulsar.Actor._loop` is up 28 | and running. 29 | * ``ACTOR_STATES.STOPPING = 4`` when :class:`pulsar.Actor.stop` has been 30 | called for the first time and the actor is running. 31 | ''' 32 | ACTOR_STATES.DESCRIPTION = {ACTOR_STATES.INACTIVE: 'inactive', 33 | ACTOR_STATES.INITIAL: 'initial', 34 | ACTOR_STATES.STARTING: 'starting', 35 | ACTOR_STATES.RUN: 'running', 36 | ACTOR_STATES.STOPPING: 'stopping', 37 | ACTOR_STATES.CLOSE: 'closed', 38 | ACTOR_STATES.TERMINATE: 'terminated'} 39 | # 40 | ACTOR_ACTION_TIMEOUT = 5 41 | '''Important constant used by :class:`pulsar.Monitor` to kill actors which 42 | don't respond to the ``stop`` command.''' 43 | 44 | MAX_ASYNC_WHILE = 1 # Max interval for async_while 45 | MIN_NOTIFY = 3 # DON'T NOTIFY BELOW THIS INTERVAL 46 | MAX_NOTIFY = 30 # NOTIFY AT LEAST AFTER THESE SECONDS 47 | ACTOR_TIMEOUT_TOLE = 0.3 # NOTIFY AFTER THIS TIMES THE TIMEOUT 48 | ACTOR_JOIN_THREAD_POOL_TIMEOUT = 5 # TIMEOUT WHEN JOINING THE THREAD POOL 49 | MONITOR_TASK_PERIOD = 5 50 | '''Interval for :class:`pulsar.Monitor` and :class:`pulsar.Arbiter` 51 | periodic task.''' 52 | -------------------------------------------------------------------------------- /pulsar/async/cov.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from multiprocessing import current_process 4 | 5 | try: 6 | import coverage 7 | except ImportError: 8 | coverage = None 9 | 10 | 11 | class Coverage: 12 | '''Coverage mixin for actors. 13 | ''' 14 | @property 15 | def coverage(self): 16 | return getattr(current_process(), '_coverage', None) 17 | 18 | def start_coverage(self): 19 | if self.cfg.coverage: 20 | if not coverage: 21 | self.logger.error('Coverage module not installed. ' 22 | 'Cannot start coverage.') 23 | return 24 | if self.is_arbiter(): 25 | if not self.coverage: 26 | self.logger.warning('Start coverage') 27 | p = current_process() 28 | p._coverage = coverage.Coverage(data_suffix=True) 29 | coverage.process_startup() 30 | p._coverage.start() 31 | config_file = self.coverage.config_file 32 | os.environ['COVERAGE_PROCESS_START'] = config_file 33 | elif self.cfg.concurrency == 'subprocess': 34 | coverage.process_startup() 35 | 36 | def stop_coverage(self): 37 | cov = self.coverage 38 | if cov and self.is_arbiter(): 39 | self.logger.warning('Saving coverage file') 40 | cov.stop() 41 | cov.save() 42 | c = coverage.Coverage() 43 | c.combine() 44 | c.save() 45 | self.stream.write( 46 | 'Coverage file available. Type "coverage html" ' 47 | 'for a report\n') 48 | -------------------------------------------------------------------------------- /pulsar/async/process.py: -------------------------------------------------------------------------------- 1 | from ..utils import autoreload 2 | from ..utils import system 3 | 4 | 5 | class ProcessMixin: 6 | 7 | def is_process(self): 8 | return True 9 | 10 | def before_start(self, actor): # pragma nocover 11 | actor.start_coverage() 12 | actor.logger.info('Booting') 13 | self._install_signals(actor) 14 | if actor.cfg.reload and self.is_arbiter(): 15 | autoreload.start() 16 | 17 | def handle_int(self, actor, sig): 18 | actor.logger.warning("got %s - stopping", system.SIG_NAMES.get(sig)) 19 | self.stop(actor, exit_code=int(sig)) 20 | 21 | handle_term = handle_int 22 | handle_quit = handle_int 23 | handle_abrt = handle_int 24 | 25 | def handle_winch(self, actor, sig): 26 | actor.logger.debug("ignore %s", system.SIG_NAMES.get(sig)) 27 | 28 | def _install_signals(self, actor): 29 | proc_name = actor.cfg.proc_name 30 | if proc_name: 31 | if not self.is_arbiter(): 32 | name = actor.name.split('.')[0] 33 | proc_name = "%s-%s" % (proc_name, name) 34 | if system.set_proctitle(proc_name): 35 | actor.logger.debug('Set process title to %s', 36 | system.get_proctitle()) 37 | system.set_owner_process(actor.cfg.uid, actor.cfg.gid) 38 | actor.logger.debug('Installing signals') 39 | loop = actor._loop 40 | for sig in system.SIGNALS: 41 | name = system.SIG_NAMES.get(sig) 42 | if name: 43 | handler = getattr(self, 'handle_%s' % name.lower(), None) 44 | if handler: 45 | loop.add_signal_handler(sig, handler, actor, sig) 46 | 47 | def _remove_signals(self, actor): 48 | actor.logger.debug('Remove signal handlers') 49 | for sig in system.SIGNALS: 50 | try: 51 | actor._loop.remove_signal_handler(sig) 52 | except Exception: 53 | pass 54 | if actor._loop.is_running(): # pragma nocover 55 | actor.logger.critical('Event loop still running when stopping') 56 | actor._loop.stop() 57 | else: 58 | actor.logger.debug('Close event loop') 59 | actor._loop.close() 60 | -------------------------------------------------------------------------------- /pulsar/async/threads.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import dummy, current_process 2 | 3 | 4 | class Thread(dummy.DummyProcess): 5 | _loop = None 6 | _pool_loop = None 7 | 8 | @property 9 | def pid(self): 10 | return current_process().pid 11 | 12 | def kill(self, sig): 13 | '''Invoke the stop on the event loop method.''' 14 | if self.is_alive() and self._loop: 15 | self._loop.call_soon_threadsafe(self._loop.stop) 16 | 17 | def set_loop(self, loop): 18 | assert self._loop is None 19 | self._loop = loop 20 | -------------------------------------------------------------------------------- /pulsar/async/timeout.py: -------------------------------------------------------------------------------- 1 | from asyncio import Task, CancelledError, TimeoutError 2 | 3 | 4 | class timeout: 5 | """timeout context manager. 6 | """ 7 | __slots__ = ('_loop', '_timeout', '_cancelled', '_task', '_cancel_handler') 8 | 9 | def __init__(self, loop, timeout): 10 | self._loop = loop 11 | self._timeout = timeout 12 | self._task = None 13 | self._cancelled = False 14 | self._cancel_handler = None 15 | 16 | def __enter__(self): 17 | if self._timeout is None: 18 | return self 19 | self._task = Task.current_task(self._loop) 20 | tm = self._loop.time() + self._timeout 21 | self._cancel_handler = self._loop.call_at(tm, self._cancel_task) 22 | return self 23 | 24 | def __exit__(self, exc_type, exc_val, exc_tb): 25 | if exc_type is CancelledError and self._cancelled: 26 | self._cancel_handler = None 27 | self._task = None 28 | raise TimeoutError 29 | if self._timeout is not None and self._cancel_handler is not None: 30 | self._cancel_handler.cancel() 31 | self._cancel_handler = None 32 | self._task = None 33 | 34 | def _cancel_task(self): 35 | self._task.cancel() 36 | self._cancelled = True 37 | -------------------------------------------------------------------------------- /pulsar/cmds/__init__.py: -------------------------------------------------------------------------------- 1 | """Useful distutils commands for continuous integration and deployment 2 | 3 | These commands works in python 2 too 4 | """ 5 | from .test import Bench, Test 6 | from .linux_wheels import ManyLinux 7 | from .pypi_version import PyPi 8 | from .s3data import S3Data 9 | 10 | 11 | __all__ = [ 12 | 'Bench', 13 | 'Test', 14 | 'ManyLinux', 15 | 'PyPi', 16 | 'S3Data' 17 | ] 18 | -------------------------------------------------------------------------------- /pulsar/cmds/build-wheels.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | PIP=${PIP:-pip} 5 | PYTHON=${PYTHON:-python} 6 | IOPATH=${IOPATH:-$PWD} 7 | DIST=${IOPATH}/dist/ 8 | WHEELRE=${PYMODULE}-*-${WHEEL}_*.whl 9 | 10 | # Compile wheels 11 | ${PIP} install --upgrade pip wheel 12 | ${PIP} install --upgrade setuptools cython 13 | ${PIP} install -r ${IOPATH}/requirements/ci.txt 14 | ${PIP} install -r ${IOPATH}/requirements/hard.txt 15 | make -C ${IOPATH} PYTHON=${PYTHON} wheels 16 | 17 | if [ $BUNDLE_WHEEL ] 18 | then 19 | echo "Bundle external shared libraries into the wheels" 20 | for whl in ${DIST}*.whl; do 21 | ${BUNDLE_WHEEL} repair $whl -w ${DIST} 22 | done 23 | fi 24 | 25 | echo "Cleanup non-$PYMODULE wheels" 26 | find ${DIST} -maxdepth 1 -type f ! -name ${WHEELRE} -print0 | xargs -0 rm -rf 27 | ls ${DIST} 28 | 29 | echo 30 | echo "unittests" 31 | ${PIP} install ${PYMODULE} --no-index -f file://${DIST} 32 | ${PIP} uninstall ${PYMODULE} -y 33 | ${PIP} install ${PYMODULE} --no-index -f file://${DIST} 34 | make -C ${IOPATH} PYTHON=${PYTHON} PIP=${PIP} wheels-test 35 | 36 | mkdir -p ${IOPATH}/wheelhouse 37 | mv ${IOPATH}/dist/*.whl ${IOPATH}/wheelhouse/ 38 | make -C ${IOPATH} clean 39 | -------------------------------------------------------------------------------- /pulsar/cmds/pypi_version.py: -------------------------------------------------------------------------------- 1 | """Check PyPI version 2 | """ 3 | from distutils.cmd import Command 4 | from distutils.errors import DistutilsError 5 | 6 | try: 7 | from xmlrpc.client import ServerProxy 8 | except ImportError: 9 | from xmlrpclib import ServerProxy 10 | 11 | 12 | class InvalidVersion(DistutilsError): 13 | pass 14 | 15 | 16 | class PyPi(Command): 17 | description = ( 18 | 'Validate version with latest PyPI version and, optionally, ' 19 | ' check if version is a valid final release' 20 | ) 21 | 22 | user_options = [ 23 | ('pypi-index-url=', None, 'PyPI index URL'), 24 | ('final', None, 25 | 'Check if version is a final release (alpha, beta, rc not allowed)') 26 | ] 27 | 28 | def initialize_options(self): 29 | self.pypi_index_url = 'https://pypi.python.org/pypi' 30 | self.final = False 31 | 32 | def finalize_options(self): 33 | pass 34 | 35 | def run(self): 36 | version = self.distribution.metadata.version 37 | if self.final: 38 | self.check_release(version) 39 | 40 | version = version.split('.') 41 | current = self.pypi_release().split('.') 42 | if version <= current: 43 | raise InvalidVersion( 44 | 'Version %s must be greater then current PyPI version %s' 45 | % (version, current) 46 | ) 47 | 48 | def pypi_release(self): 49 | """Get the latest pypi release 50 | """ 51 | meta = self.distribution.metadata 52 | pypi = ServerProxy(self.pypi_index_url) 53 | releases = pypi.package_releases(meta.name) 54 | if releases: 55 | return next(iter(sorted(releases, reverse=True))) 56 | 57 | def check_release(self, version): 58 | try: 59 | vtuple = list(map(int, version.split('.'))) 60 | assert len(vtuple) == 3 61 | except Exception: 62 | raise InvalidVersion( 63 | "Not a valid final release version %s" % version) 64 | -------------------------------------------------------------------------------- /pulsar/utils/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Documentation for utilities used by pulsar internals. This module is 3 | independent from all other pulsar modules and therefore can be used 4 | as a stand-alone library. 5 | 6 | HTTP 7 | ============ 8 | 9 | .. automodule:: pulsar.utils.httpurl 10 | 11 | 12 | .. _tools-ws-parser: 13 | 14 | Websocket 15 | ============================== 16 | 17 | .. automodule:: pulsar.utils.websocket 18 | 19 | 20 | .. _api-config: 21 | 22 | Configuration 23 | ================== 24 | 25 | .. automodule:: pulsar.utils.config 26 | 27 | 28 | .. _socket_address: 29 | 30 | Socket and addresses 31 | ======================= 32 | 33 | .. automodule:: pulsar.utils.internet 34 | :members: 35 | 36 | Internals 37 | ======================= 38 | 39 | .. module:: pulsar.utils.system 40 | 41 | System info 42 | ~~~~~~~~~~~~~~~~~ 43 | 44 | .. autofunction:: process_info 45 | 46 | 47 | .. module:: pulsar.utils.tools 48 | 49 | Check arity 50 | ~~~~~~~~~~~~~~~~~ 51 | 52 | .. autofunction:: checkarity 53 | 54 | 55 | Structures 56 | ================== 57 | 58 | .. automodule:: pulsar.utils.structures 59 | 60 | 61 | HTML & Text 62 | ================== 63 | 64 | .. automodule:: pulsar.utils.html 65 | :members: 66 | 67 | Slugify 68 | ================== 69 | 70 | .. automodule:: pulsar.utils.slugify 71 | 72 | 73 | Logging 74 | ================== 75 | 76 | .. automodule:: pulsar.utils.log 77 | :members: 78 | 79 | 80 | Path 81 | ================== 82 | 83 | .. automodule:: pulsar.utils.path 84 | :members: 85 | 86 | ''' 87 | -------------------------------------------------------------------------------- /pulsar/utils/http/__init__.py: -------------------------------------------------------------------------------- 1 | from ..lib import HAS_C_EXTENSIONS 2 | 3 | 4 | if HAS_C_EXTENSIONS: 5 | try: 6 | from httptools import ( 7 | HttpResponseParser, HttpRequestParser, HttpParserUpgrade, 8 | HttpParserError, parse_url 9 | ) 10 | hasextensions = True 11 | except ImportError: 12 | hasextensions = False 13 | 14 | else: 15 | hasextensions = False 16 | 17 | 18 | if not hasextensions: 19 | from .parser import ( # noqa 20 | HttpRequestParser, HttpResponseParser, HttpParserUpgrade, 21 | HttpParserError, parse_url 22 | ) 23 | 24 | 25 | CHARSET = 'ISO-8859-1' 26 | 27 | 28 | __all__ = [ 29 | 'HttpResponseParser', 30 | 'HttpRequestParser', 31 | 'HttpParserUpgrade', 32 | 'HttpParserError', 33 | 'parse_url', 34 | 'hasextensions', 35 | 'CHARSET' 36 | ] 37 | -------------------------------------------------------------------------------- /pulsar/utils/lib.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if os.environ.get('PULSARPY', 'no') == 'yes': 4 | HAS_C_EXTENSIONS = False 5 | else: 6 | HAS_C_EXTENSIONS = True 7 | try: 8 | import httptools # noqa 9 | from .clib import ( 10 | EventHandler, ProtocolConsumer, Protocol, Producer, WsgiProtocol, 11 | AbortEvent, RedisParser, WsgiResponse, wsgi_cached, http_date, 12 | FrameParser, has_empty_content, isawaitable, Event 13 | ) 14 | except ImportError: 15 | HAS_C_EXTENSIONS = False 16 | 17 | 18 | if not HAS_C_EXTENSIONS: 19 | from inspect import isawaitable # noqa 20 | 21 | from .pylib.protocols import ProtocolConsumer, Protocol, Producer # noqa 22 | from .pylib.events import EventHandler, AbortEvent, Event # noqa 23 | from .pylib.wsgi import WsgiProtocol, http_date, has_empty_content # noqa 24 | from .pylib.wsgiresponse import WsgiResponse, wsgi_cached # noqa 25 | from .pylib.redisparser import RedisParser # noqa 26 | from .pylib.websocket import FrameParser # noqa 27 | 28 | 29 | __all__ = [ 30 | 'HAS_C_EXTENSIONS', 31 | 'AbortEvent', 32 | 'EventHandler', 33 | 'Event', 34 | 'ProtocolConsumer', 35 | 'Protocol', 36 | 'WsgiProtocol', 37 | 'WsgiResponse', 38 | 'wsgi_cached', 39 | 'http_date', 40 | 'isawaitable', 41 | 'has_empty_content', 42 | 'RedisParser' 43 | ] 44 | -------------------------------------------------------------------------------- /pulsar/utils/profiler.py: -------------------------------------------------------------------------------- 1 | import cProfile 2 | import pstats 3 | 4 | 5 | class Profiler: 6 | 7 | def __init__(self, sortby=None): 8 | self.profiler = None 9 | self.sortby = sortby or ('cumtime',) 10 | 11 | def __enter__(self): 12 | self.profiler = cProfile.Profile() 13 | self.profiler.enable() 14 | return self 15 | 16 | def __exit__(self, exc_type, exc_val, exc_tb): 17 | self.profiler.disable() 18 | self.write_stats() 19 | 20 | def write_stats(self): 21 | p = pstats.Stats(self.profiler) 22 | p.strip_dirs().sort_stats(*self.sortby).print_stats() 23 | -------------------------------------------------------------------------------- /pulsar/utils/pylib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/pulsar/utils/pylib/__init__.py -------------------------------------------------------------------------------- /pulsar/utils/security.py: -------------------------------------------------------------------------------- 1 | """\ 2 | Security related helpers such as secure password hashing tools. 3 | """ 4 | from hashlib import sha1 5 | import string 6 | from random import SystemRandom 7 | 8 | from .httpurl import ascii_letters 9 | from .string import to_bytes 10 | 11 | SALT_CHARS = ascii_letters + string.digits 12 | 13 | 14 | _sys_rng = SystemRandom() 15 | 16 | 17 | def gen_salt(length): 18 | """Generate a random string of SALT_CHARS with specified ``length``.""" 19 | if length <= 0: 20 | raise ValueError('requested salt of length <= 0') 21 | return ''.join(_sys_rng.choice(SALT_CHARS) for _ in range(length)) 22 | 23 | 24 | def _hash_internal(salt, password): 25 | return sha1(('%s%s' % (salt, password)).encode('utf-8')).hexdigest() 26 | 27 | 28 | def generate_password_hash(password, salt_length=8): 29 | salt = gen_salt(salt_length) 30 | h = _hash_internal(salt, password) 31 | return '%s$%s' % (salt, h) 32 | 33 | 34 | def check_password_hash(pwhash, password): 35 | if pwhash.count('$') != 1: 36 | return False 37 | salt, hashval = pwhash.split('$') 38 | return _hash_internal(salt, password) == hashval 39 | 40 | 41 | def digest(text): 42 | return sha1(to_bytes(text)).hexdigest() 43 | -------------------------------------------------------------------------------- /pulsar/utils/string.py: -------------------------------------------------------------------------------- 1 | import string 2 | import re 3 | from uuid import uuid4 4 | from random import randint, choice 5 | 6 | 7 | _characters = string.ascii_letters + string.digits 8 | 9 | 10 | def to_bytes(s, encoding=None, errors=None): 11 | '''Convert *s* into bytes''' 12 | if not isinstance(s, bytes): 13 | return ('%s' % s).encode(encoding or 'utf-8', errors or 'strict') 14 | elif not encoding or encoding == 'utf-8': 15 | return s 16 | else: 17 | d = s.decode('utf-8') 18 | return d.encode(encoding, errors or 'strict') 19 | 20 | 21 | def to_string(s, encoding=None, errors='strict'): 22 | """Inverse of to_bytes""" 23 | if isinstance(s, bytes): 24 | return s.decode(encoding or 'utf-8', errors) 25 | elif not isinstance(s, str): 26 | return str(s) 27 | else: 28 | return s 29 | 30 | 31 | def native_str(s, encoding=None): 32 | if isinstance(s, bytes): 33 | return s.decode(encoding or 'utf-8') 34 | else: 35 | return s 36 | 37 | 38 | def random_string(min_len=3, max_len=20, characters=None, **kwargs): 39 | characters = characters or _characters 40 | len = randint(min_len, max_len) if max_len > min_len else min_len 41 | return ''.join((choice(characters) for s in range(len))) 42 | 43 | 44 | def gen_unique_id(): 45 | return 'i%s' % uuid4().hex 46 | 47 | 48 | def camel_to_dash(name): 49 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 50 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 51 | -------------------------------------------------------------------------------- /pulsar/utils/structures/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Collection of data structures and function used throughout the library. 3 | 4 | .. module:: pulsar.utils.structures.misc 5 | 6 | MultiValueDict 7 | ~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | .. autoclass:: MultiValueDict 10 | :members: 11 | :member-order: bysource 12 | 13 | 14 | .. _attribute-dictionary: 15 | 16 | AttributeDictionary 17 | ~~~~~~~~~~~~~~~~~~~~~~~ 18 | 19 | .. autoclass:: AttributeDictionary 20 | :members: 21 | :member-order: bysource 22 | 23 | 24 | FrozenDict 25 | ~~~~~~~~~~~~~~~~~~~~~~~ 26 | 27 | .. autoclass:: FrozenDict 28 | :members: 29 | :member-order: bysource 30 | 31 | 32 | .. module:: pulsar.utils.structures.skiplist 33 | 34 | Skiplist 35 | ~~~~~~~~~~~~~~~ 36 | .. autoclass:: Skiplist 37 | :members: 38 | :member-order: bysource 39 | 40 | 41 | .. module:: pulsar.utils.structures.zset 42 | 43 | Zset 44 | ~~~~~~~~~~~~~~~ 45 | .. autoclass:: Zset 46 | :members: 47 | :member-order: bysource 48 | ''' 49 | from .skiplist import Skiplist 50 | from .zset import Zset 51 | from .misc import ( 52 | AttributeDictionary, FrozenDict, Dict, Deque, recursive_update, 53 | mapping_iterator, inverse_mapping, aslist, as_tuple 54 | ) 55 | 56 | 57 | __all__ = [ 58 | 'Skiplist', 59 | 'Zset', 60 | 'AttributeDictionary', 61 | 'FrozenDict', 62 | 'Dict', 63 | 'Deque', 64 | 'recursive_update', 65 | 'mapping_iterator', 66 | 'inverse_mapping', 67 | 'aslist', 68 | 'as_tuple' 69 | ] 70 | -------------------------------------------------------------------------------- /pulsar/utils/system/__init__.py: -------------------------------------------------------------------------------- 1 | '''Operative system specific functions and classes. 2 | ''' 3 | import os 4 | 5 | from .runtime import Platform 6 | from .base import * # noqa 7 | 8 | platform = Platform() 9 | seconds = platform.seconds 10 | 11 | if platform.type == 'win': # pragma nocover 12 | from .windowssystem import * # noqa 13 | else: 14 | from .posixsystem import * # noqa 15 | 16 | 17 | try: 18 | import psutil 19 | except ImportError: # pragma nocover 20 | psutil = None 21 | 22 | try: 23 | import ujson as json # noqa 24 | except ImportError: # pragma nocover 25 | import json # noqa 26 | 27 | 28 | memory_symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') 29 | memory_size = dict(((s, 1 << (i+1)*10) for i, s in enumerate(memory_symbols))) 30 | 31 | 32 | def convert_bytes(b): 33 | '''Convert a number of bytes into a human readable memory usage, bytes, 34 | kilo, mega, giga, tera, peta, exa, zetta, yotta''' 35 | if b is None: 36 | return '#NA' 37 | for s in reversed(memory_symbols): 38 | if b >= memory_size[s]: 39 | value = float(b) / memory_size[s] 40 | return '%.1f%sB' % (value, s) 41 | return "%sB" % b 42 | 43 | 44 | def process_info(pid=None): 45 | '''Returns a dictionary of system information for the process ``pid``. 46 | 47 | It uses the psutil_ module for the purpose. If psutil_ is not available 48 | it returns an empty dictionary. 49 | 50 | .. _psutil: http://code.google.com/p/psutil/ 51 | ''' 52 | if psutil is None: # pragma nocover 53 | return {} 54 | pid = pid or os.getpid() 55 | try: 56 | p = psutil.Process(pid) 57 | # this fails on platforms which don't allow multiprocessing 58 | except psutil.NoSuchProcess: # pragma nocover 59 | return {} 60 | else: 61 | mem = p.memory_info() 62 | return {'memory': convert_bytes(mem.rss), 63 | 'memory_virtual': convert_bytes(mem.vms), 64 | 'cpu_percent': p.cpu_percent(), 65 | 'nice': p.nice(), 66 | 'num_threads': p.num_threads()} 67 | -------------------------------------------------------------------------------- /pulsar/utils/system/base.py: -------------------------------------------------------------------------------- 1 | try: 2 | import signal 3 | except ImportError: # pragma nocover 4 | signal = None 5 | 6 | 7 | __all__ = ['SIG_NAMES', 8 | 'set_proctitle', 9 | 'get_proctitle'] 10 | 11 | 12 | SIG_NAMES = {} 13 | 14 | 15 | if signal: 16 | for sig in dir(signal): 17 | if sig.startswith('SIG') and sig[3] != "_": 18 | val = getattr(signal, sig) 19 | if isinstance(val, int): 20 | SIG_NAMES[val] = sig[3:] 21 | 22 | 23 | try: 24 | from setproctitle import setproctitle, getproctitle 25 | 26 | def set_proctitle(title): 27 | setproctitle(title) 28 | return True 29 | 30 | def get_proctitle(): 31 | return getproctitle() 32 | 33 | except ImportError: # pragma nocover 34 | 35 | def set_proctitle(title): 36 | return False 37 | 38 | def get_proctitle(): 39 | pass 40 | -------------------------------------------------------------------------------- /pulsar/utils/system/runtime.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import socket 5 | 6 | 7 | known_platforms = { 8 | 'nt': 'win', 9 | 'ce': 'win', 10 | 'posix': 'posix', 11 | 'java': 'java', 12 | 'org.python.modules.os': 'java', 13 | } 14 | 15 | _timeFunctions = { 16 | # 'win': time.clock, 17 | 'win': time.time, 18 | } 19 | 20 | 21 | class Platform: 22 | """Gives us information about the platform we're running on""" 23 | 24 | name = os.name 25 | type = known_platforms.get(os.name) 26 | is_windows = (type == 'win') 27 | is_macosx = (sys.platform == 'darwin') 28 | is_posix = (type == 'posix') 29 | has_multiprocessing_socket = hasattr(socket, 'fromfd') 30 | seconds = staticmethod(_timeFunctions.get(type, time.time)) 31 | 32 | def __repr__(self): 33 | return '{0} - {1}'.format(self.type, self.name) 34 | __str__ = __repr__ 35 | -------------------------------------------------------------------------------- /pulsar/utils/system/windowssystem.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import ctypes 4 | import ctypes.wintypes 5 | import getpass 6 | from multiprocessing import current_process 7 | 8 | from .base import * # noqa 9 | 10 | __all__ = ['daemonize', 11 | 'EXIT_SIGNALS', 12 | 'SIGNALS', 13 | 'kill', 14 | 'get_uid', 15 | 'get_gid', 16 | 'get_maxfd', 17 | 'set_owner_process', 18 | 'current_process'] 19 | 20 | # See: http://msdn.microsoft.com/en-us/library/ms724935(VS.85).aspx 21 | SetHandleInformation = ctypes.windll.kernel32.SetHandleInformation 22 | SetHandleInformation.argtypes = (ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, 23 | ctypes.wintypes.DWORD) 24 | SetHandleInformation.restype = ctypes.wintypes.BOOL 25 | 26 | MAXFD = 1024 27 | HANDLE_FLAG_INHERIT = 0x00000001 28 | EXIT_SIGNALS = (signal.SIGINT, signal.SIGTERM, signal.SIGABRT, signal.SIGBREAK) 29 | SIGNALS = () 30 | 31 | 32 | def kill(pid, sig): 33 | os.kill(pid, sig) 34 | 35 | 36 | def set_owner_process(gid, uid): 37 | return None 38 | 39 | 40 | def get_parent_id(): 41 | try: 42 | return os.getppid() 43 | except Exception: 44 | return None 45 | 46 | 47 | def chown(path, uid, gid): 48 | pass 49 | 50 | 51 | def _set_non_blocking(fd): 52 | pass 53 | 54 | 55 | def get_uid(user=None): 56 | if not user: 57 | return getpass.getuser() 58 | elif user == getpass.getuser(): 59 | return user 60 | 61 | 62 | def get_gid(group=None): 63 | return None 64 | 65 | 66 | def setpgrp(): 67 | pass 68 | 69 | 70 | def get_maxfd(): 71 | return MAXFD 72 | 73 | 74 | def daemonize(): 75 | pass 76 | -------------------------------------------------------------------------------- /pulsar/utils/system/winprocess.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from multiprocessing import forking, process, freeze_support 4 | from multiprocessing.util import _logger, _log_to_stderr 5 | 6 | WINEXE = forking.WINEXE 7 | 8 | 9 | def get_preparation_data(name): 10 | ''' 11 | Return info about parent needed by child to unpickle process object. 12 | Monkey-patch from 13 | ''' 14 | d = dict( 15 | name=name, 16 | sys_path=sys.path, 17 | sys_argv=sys.argv, 18 | log_to_stderr=_log_to_stderr, 19 | orig_dir=process.ORIGINAL_DIR, 20 | authkey=process.current_process().authkey, 21 | ) 22 | 23 | if _logger is not None: 24 | d['log_level'] = _logger.getEffectiveLevel() 25 | 26 | if not WINEXE: 27 | main_path = getattr(sys.modules['__main__'], '__file__', None) 28 | if not main_path and sys.argv[0] not in ('', '-c'): 29 | main_path = sys.argv[0] 30 | if main_path is not None: 31 | if (not os.path.isabs(main_path) and process.ORIGINAL_DIR 32 | is not None): 33 | main_path = os.path.join(process.ORIGINAL_DIR, main_path) 34 | if not main_path.endswith('.exe'): 35 | d['main_path'] = os.path.normpath(main_path) 36 | 37 | return d 38 | 39 | 40 | forking.get_preparation_data = get_preparation_data 41 | freeze_support() 42 | -------------------------------------------------------------------------------- /pulsar/utils/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .arity import * # noqa 2 | from .pidfile import * # noqa 3 | from .text import * # noqa 4 | from .numbers import * # noqa 5 | -------------------------------------------------------------------------------- /pulsar/utils/tools/numbers.py: -------------------------------------------------------------------------------- 1 | from time import mktime 2 | from datetime import datetime 3 | 4 | 5 | __all__ = ['date2timestamp'] 6 | 7 | 8 | def date2timestamp(dte): 9 | '''Convert a *dte* into a valid unix timestamp.''' 10 | seconds = mktime(dte.timetuple()) 11 | if isinstance(dte, datetime): 12 | return seconds + dte.microsecond / 1000000.0 13 | else: 14 | return int(seconds) 15 | -------------------------------------------------------------------------------- /pulsar/utils/tools/pidfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | __all__ = ['Pidfile'] 5 | 6 | 7 | class Pidfile: 8 | """\ 9 | Manage a PID file. If a specific name is provided 10 | it and '"%s.oldpid" % name' will be used. Otherwise 11 | we create a temp file using os.mkstemp. 12 | """ 13 | def __init__(self, fname=None): 14 | self.fname = fname 15 | self.pid = None 16 | 17 | def create(self, pid=None): 18 | pid = pid or os.getpid() 19 | self.pid = self.read() 20 | if self.pid and self.pid != pid: 21 | if self.exists: 22 | raise RuntimeError("Already running on PID %s " 23 | "(pid file '%s')" % 24 | (self.pid, self.fname)) 25 | self.pid = pid 26 | # Write pidfile 27 | if self.fname: 28 | fdir = os.path.dirname(self.fname) 29 | if fdir and not os.path.isdir(fdir): 30 | raise RuntimeError("%s doesn't exist. Can't create pidfile." 31 | % fdir) 32 | else: 33 | self.fname = tempfile.mktemp() 34 | with open(self.fname, 'w') as f: 35 | f.write("%s\n" % self.pid) 36 | # set permissions to -rw-r--r-- 37 | os.chmod(self.fname, 420) 38 | 39 | def rename(self, path): 40 | self.unlink() 41 | self.fname = path 42 | self.create(self.pid) 43 | 44 | def unlink(self): 45 | """ delete pidfile""" 46 | try: 47 | with open(self.fname, "r") as f: 48 | pid1 = int(f.read() or 0) 49 | if pid1 == self.pid: 50 | os.unlink(self.fname) 51 | except Exception: 52 | pass 53 | 54 | def read(self): 55 | """ Validate pidfile and make it stale if needed""" 56 | if not self.fname: 57 | return 58 | try: 59 | with open(self.fname, "r") as f: 60 | wpid = int(f.read() or 0) 61 | if wpid <= 0: 62 | return 63 | return wpid 64 | except IOError: 65 | return 66 | 67 | @property 68 | def exists(self): 69 | if self.pid: 70 | try: 71 | os.kill(self.pid, 0) 72 | return True 73 | except ProcessLookupError: 74 | pass 75 | return False 76 | -------------------------------------------------------------------------------- /pulsar/utils/wsgi_py.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/pulsar/utils/wsgi_py.py -------------------------------------------------------------------------------- /requirements/ci.txt: -------------------------------------------------------------------------------- 1 | docker 2 | botocore 3 | httptools 4 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | -r ci.txt 2 | -r test-posix.txt 3 | setproctitle 4 | psutil 5 | unidecode 6 | pyslink 7 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | -r hard.txt 2 | recommonmark 3 | flask 4 | greenlet 5 | -------------------------------------------------------------------------------- /requirements/hard.txt: -------------------------------------------------------------------------------- 1 | multidict 2 | -------------------------------------------------------------------------------- /requirements/test-docs.txt: -------------------------------------------------------------------------------- 1 | -r docs.txt 2 | sphinxcontrib-spelling 3 | -------------------------------------------------------------------------------- /requirements/test-posix.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | cython 3 | uvloop 4 | httptools 5 | -------------------------------------------------------------------------------- /requirements/test-win.txt: -------------------------------------------------------------------------------- 1 | -r test.txt 2 | botocore 3 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | coverage 3 | codecov 4 | greenlet 5 | flask 6 | oauthlib 7 | requests 8 | unidecode 9 | certifi 10 | twine 11 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | 2 | if __name__ == '__main__': 3 | from pulsar.apps.test import TestSuite 4 | test_suite = TestSuite(test_modules=('tests', 'examples')) 5 | test_suite.start() 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = clean sdist bdist_wheel upload 3 | 4 | [pep8] 5 | exclude = .git,.eggs,.idea,__pycache__,dist,build,docs,examples/node_modules,venv 6 | 7 | [flake8] 8 | exclude = .git,.eggs,.idea,__pycache__,dist,build,docs,examples/node_modules,venv 9 | 10 | [test] 11 | test_modules = tests examples 12 | test_timeout = 60 13 | 14 | [bench] 15 | test_modules = tests/bench 16 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/tests/__init__.py -------------------------------------------------------------------------------- /tests/apps/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def dummy(environ, start_response): 4 | start_response('200 OK', []) 5 | yield [b'dummy'] 6 | -------------------------------------------------------------------------------- /tests/apps/test_rpc.py: -------------------------------------------------------------------------------- 1 | '''Tests the rpc middleware and utilities. It uses the calculator example.''' 2 | import unittest 3 | 4 | from pulsar.apps import rpc 5 | from pulsar.apps.http import HttpWsgiClient 6 | 7 | 8 | class rpcTest(unittest.TestCase): 9 | 10 | def proxy(self): 11 | from examples.calculator.manage import Site 12 | http = HttpWsgiClient(Site()) 13 | return rpc.JsonProxy('http://127.0.0.1:8060/', http=http, timeout=20) 14 | 15 | def test_proxy(self): 16 | p = self.proxy() 17 | http = p.http 18 | self.assertTrue(len(http.headers)) 19 | self.assertEqual(http.headers['user-agent'], 'Pulsar-Http-Wsgi-Client') 20 | self.assertTrue(http.wsgi_callable) 21 | self.assertEqual(p._version, '2.0') 22 | 23 | async def test_addition(self): 24 | p = self.proxy() 25 | response = await p.calc.add(4, 5) 26 | self.assertEqual(response, 9) 27 | -------------------------------------------------------------------------------- /tests/async/__init__.py: -------------------------------------------------------------------------------- 1 | import signal 2 | from time import time 3 | 4 | from pulsar.api import send, spawn, get_application, create_future, arbiter 5 | 6 | 7 | def add(actor, a, b): 8 | return actor.name, a+b 9 | 10 | 11 | def wait_for_stop(test, aid, terminating=False): 12 | '''Wait for an actor to stop''' 13 | actor = arbiter() 14 | waiter = create_future(loop=actor._loop) 15 | 16 | def remove(): 17 | test.assertEqual(actor.event('periodic_task').unbind(check), 1) 18 | waiter.set_result(None) 19 | 20 | def check(caller, **kw): 21 | test.assertEqual(caller, actor) 22 | if not terminating: 23 | test.assertFalse(aid in actor.managed_actors) 24 | elif aid in actor.managed_actors: 25 | return 26 | actor._loop.call_soon(remove) 27 | 28 | actor.event('periodic_task').bind(check) 29 | return waiter 30 | 31 | 32 | async def get_test(_): 33 | app = await get_application('test') 34 | return app.cfg 35 | 36 | 37 | def check_environ(actor, name): 38 | import os 39 | return os.environ.get(name) 40 | 41 | 42 | async def spawn_actor_from_actor(actor, name): 43 | actor2 = await spawn(name=name) 44 | pong = await send(actor2, 'ping') 45 | assert pong == 'pong', 'no pong from actor' 46 | t1 = time() 47 | # cover the notify from a fron actor 48 | t2 = await send(actor2, 'notify', {}) 49 | assert t2 >= t1 50 | 51 | return actor2.aid 52 | 53 | 54 | def cause_timeout(actor): 55 | actor.cfg.set('timeout', 10*actor.cfg.timeout) 56 | 57 | 58 | def cause_terminate(actor): 59 | actor.cfg.set('timeout', 100*actor.cfg.timeout) 60 | actor.concurrency.kill = kill_hack(actor.concurrency.kill) 61 | actor.stop = lambda exc=None, exit_code=None: False 62 | 63 | 64 | def kill_hack(kill): 65 | 66 | def _(sig): 67 | if sig == signal.SIGKILL: 68 | kill(sig) 69 | 70 | return _ 71 | 72 | 73 | def close_mailbox(actor, close=False): 74 | if not close: 75 | actor._loop.call_later(0.5, close_mailbox, actor, True) 76 | return True 77 | # just for coverage 78 | assert repr(actor.mailbox) 79 | # close mailbox 80 | actor.mailbox.close() 81 | -------------------------------------------------------------------------------- /tests/async/test_actor_coroutine.py: -------------------------------------------------------------------------------- 1 | # import unittest 2 | 3 | # from tests.async.actor import ActorTest 4 | 5 | 6 | # class TestActorThread(ActorTest, unittest.TestCase): 7 | # concurrency = 'coroutine' 8 | -------------------------------------------------------------------------------- /tests/async/test_actor_process.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.apps.test import dont_run_with_thread 4 | 5 | from tests.async.actor import ActorTest 6 | 7 | 8 | @dont_run_with_thread 9 | class TestActorMultiProcess(ActorTest, unittest.TestCase): 10 | concurrency = 'process' 11 | -------------------------------------------------------------------------------- /tests/async/test_actor_subprocess.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.apps.test import dont_run_with_thread, skipUnless 4 | from pulsar.utils.system import platform 5 | 6 | from tests.async.actor import ActorTest 7 | 8 | 9 | @dont_run_with_thread 10 | @skipUnless(platform.type != 'win', 'Requires posix OS') 11 | class TestActorMultiProcess(ActorTest, unittest.TestCase): 12 | concurrency = 'subprocess' 13 | -------------------------------------------------------------------------------- /tests/async/test_actor_thread.py: -------------------------------------------------------------------------------- 1 | '''Tests actor and actor proxies.''' 2 | import unittest 3 | 4 | from tests.async.actor import ActorTest 5 | 6 | 7 | class TestActorThread(ActorTest, unittest.TestCase): 8 | concurrency = 'thread' 9 | -------------------------------------------------------------------------------- /tests/async/test_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.api import get_proxy, spawn, send, CommandNotFound 4 | 5 | 6 | class TestApi(unittest.TestCase): 7 | 8 | def test_get_proxy(self): 9 | self.assertRaises(ValueError, get_proxy, 'shcbjsbcjcdcd') 10 | self.assertEqual(get_proxy('shcbjsbcjcdcd', safe=True), None) 11 | 12 | async def test_bad_concurrency(self): 13 | # bla concurrency does not exists 14 | with self.assertRaises(ValueError): 15 | await spawn(kind='bla') 16 | 17 | async def test_actor_coverage(self): 18 | with self.assertRaises(CommandNotFound): 19 | await send('arbiter', 'sjdcbhjscbhjdbjsj', 'bla') 20 | -------------------------------------------------------------------------------- /tests/async/test_events.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.api import EventHandler 4 | 5 | 6 | class Handler(EventHandler): 7 | ONE_TIME_EVENTS = ('start', 'finish') 8 | 9 | 10 | class TestFailure(unittest.TestCase): 11 | 12 | def test_one_time(self): 13 | h = Handler() 14 | e = h.event('finish') 15 | self.assertTrue(e.onetime()) 16 | e.fire() 17 | self.assertTrue(e.fired()) 18 | self.assertEqual(e.handlers(), None) 19 | 20 | def test_one_time_error(self): 21 | h = Handler() 22 | h.event('finish').bind(lambda f, exc=None: 'OK'+4) 23 | with self.assertRaises(TypeError): 24 | h.event('finish').fire() 25 | 26 | def test_bind_events(self): 27 | h = Handler() 28 | h.bind_events({'foo': 3, 'bla': 6}) 29 | self.assertFalse(h.event('start').handlers()) 30 | self.assertFalse(h.event('finish').handlers()) 31 | h.bind_events({'start': lambda r, data=None, exc=None: data+1, 32 | 'finish': lambda r, data=None, exc=None: data+1}) 33 | self.assertTrue(h.event('start').handlers()) 34 | self.assertTrue(h.event('finish').handlers()) 35 | h.event('start').fire(data=1) 36 | self.assertTrue(h.event('start').fired()) 37 | self.assertRaises(RuntimeError, h.event('start').bind, lambda: None) 38 | 39 | def test_unbind(self): 40 | h = Handler() 41 | 42 | def cbk(_, **kw): 43 | return kw 44 | 45 | h.event('many').bind(cbk) 46 | e = h.event('many') 47 | self.assertTrue(e) 48 | self.assertEqual(h.event('foo').unbind(cbk), 0) 49 | self.assertEqual(e.unbind(cbk), 1) 50 | self.assertEqual(e.unbind(cbk), 0) 51 | self.assertEqual(e.handlers(), []) 52 | 53 | def test_copy_many_times_events(self): 54 | h = EventHandler() 55 | 56 | def cbk(_, **kw): 57 | return kw 58 | 59 | h.event('start').bind(cbk) 60 | self.assertEqual(h.event('start').onetime(), False) 61 | h2 = Handler() 62 | self.assertEqual(h2.event('start').onetime(), True) 63 | h2.copy_many_times_events(h) 64 | self.assertEqual(h2.event('start').handlers(), [cbk]) 65 | -------------------------------------------------------------------------------- /tests/async/test_lock.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | 4 | from pulsar.api import Lock 5 | 6 | 7 | class TestLock(unittest.TestCase): 8 | 9 | def test_lock_init(self): 10 | lock = Lock('foo') 11 | self.assertEqual(lock.name, 'foo') 12 | lock2 = Lock('foo') 13 | self.assertEqual(lock2.name, 'foo') 14 | self.assertNotEqual(lock, lock2) 15 | self.assertEqual(lock._lock, lock2._lock) 16 | 17 | async def test_lock(self): 18 | lock1 = Lock('test', blocking=False) 19 | lock2 = Lock('test', blocking=False) 20 | self.assertEqual(await lock1.acquire(), True) 21 | self.assertEqual(await lock2.acquire(), False) 22 | self.assertFalse(lock2.locked()) 23 | await lock1.release() 24 | self.assertFalse(lock1.locked()) 25 | 26 | async def test_lock_blocking(self): 27 | lock1 = Lock('test1') 28 | lock2 = Lock('test1', blocking=1) 29 | self.assertEqual(await lock1.acquire(), True) 30 | start = lock2._loop.time() 31 | self.assertEqual(await lock2.acquire(), False) 32 | self.assertGreaterEqual(lock2._loop.time() - start, 1) 33 | self.assertFalse(lock2.locked()) 34 | await lock1.release() 35 | self.assertFalse(lock1.locked()) 36 | 37 | async def test_lock_timeout(self): 38 | lock = Lock('test2', timeout=1) 39 | self.assertEqual(await lock.acquire(), True) 40 | await asyncio.sleep(1.5) 41 | self.assertFalse(lock.locked()) 42 | 43 | async def test_lock_timeout_lock(self): 44 | lock1 = Lock('test3', timeout=1) 45 | lock2 = Lock('test3', blocking=True) 46 | self.assertEqual(await lock1.acquire(), True) 47 | self.assertTrue(lock1.locked()) 48 | future = asyncio.ensure_future(lock2.acquire()) 49 | await asyncio.sleep(1.5) 50 | self.assertFalse(lock1.locked()) 51 | await future 52 | self.assertTrue(lock2.locked()) 53 | 54 | async def test_context(self): 55 | lock = Lock('test4', blocking=1) 56 | async with Lock('test4'): 57 | self.assertEqual(await lock.acquire(), False) 58 | self.assertFalse(lock.locked()) 59 | async with lock: 60 | self.assertTrue(lock.locked()) 61 | self.assertFalse(lock.locked()) 62 | -------------------------------------------------------------------------------- /tests/bench/test_coroutine.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from asyncio import get_event_loop 3 | 4 | from pulsar.api import create_future 5 | 6 | 7 | DELAY = 0 8 | 9 | 10 | def async_func(loop, value): 11 | p = create_future(loop) 12 | loop.call_later(DELAY, p.set_result, value) 13 | return p 14 | 15 | 16 | async def sub_sub(loop, num): 17 | a = await async_func(loop, num) 18 | b = await async_func(loop, num) 19 | return a+b 20 | 21 | 22 | async def sub(loop, num): 23 | return ( 24 | await async_func(loop, num) + 25 | await async_func(loop, num) + 26 | await sub_sub(loop, num) 27 | ) 28 | 29 | 30 | async def main(num, loop=None): 31 | loop = loop or get_event_loop() 32 | a = await async_func(loop, num) 33 | b = await sub(loop, num) 34 | c = await sub(loop, num) 35 | return a+b+c 36 | 37 | 38 | class TestCoroutine(unittest.TestCase): 39 | __benchmark__ = True 40 | __number__ = 1000 41 | 42 | async def test_coroutine(self): 43 | result = await main(1) 44 | self.assertEqual(result, 9) 45 | -------------------------------------------------------------------------------- /tests/bench/test_events.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.utils.pylib.events import EventHandler as PyEventHandler 4 | from pulsar.api import EventHandler 5 | 6 | 7 | class TestPythonCode(unittest.TestCase): 8 | __benchmark__ = True 9 | __number__ = 10000 10 | 11 | @classmethod 12 | async def setUpClass(cls): 13 | cls.hnd = EventHandler() 14 | cls.py_hnd = PyEventHandler() 15 | cls.hnd.event('bang').bind(cls.bang) 16 | cls.py_hnd.event('bang').bind(cls.bang) 17 | 18 | def test_bind(self): 19 | self.hnd.event('foo').bind(self.foo) 20 | 21 | def test_fire(self): 22 | self.hnd.fire_event('bang') 23 | 24 | def test_fire_data(self): 25 | self.hnd.fire_event('bang', data=self) 26 | 27 | def test_bind_py(self): 28 | self.py_hnd.event('foo').bind(self.foo) 29 | 30 | def test_fire_py(self): 31 | self.py_hnd.fire_event('bang') 32 | 33 | def test_fire_data_py(self): 34 | self.py_hnd.fire_event('bang', data=self) 35 | 36 | @classmethod 37 | def foo(cls, o): 38 | pass 39 | 40 | @classmethod 41 | def bang(cls, o, data=None): 42 | pass 43 | -------------------------------------------------------------------------------- /tests/bench/test_green.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.api import send 4 | 5 | try: 6 | from pulsar.apps import greenio 7 | run_in_greenlet = greenio.run_in_greenlet 8 | except ImportError: 9 | greenio = None 10 | 11 | def run_in_greenlet(f): 12 | return f 13 | 14 | 15 | from examples.echo.manage import server, Echo 16 | 17 | 18 | class EchoGreen(Echo): 19 | '''An echo client which uses greenlets to provide implicit 20 | asynchronous code''' 21 | 22 | def __call__(self, message): 23 | return greenio.wait(super().__call__(message)) 24 | 25 | 26 | @unittest.skipUnless(greenio, "Requires the greenlet module") 27 | class TestGreenIo(unittest.TestCase): 28 | __benchmark__ = True 29 | __number__ = 2000 30 | 31 | @classmethod 32 | async def setUpClass(cls): 33 | s = server(name=cls.__name__.lower(), bind='127.0.0.1:0') 34 | cls.server_cfg = await send('arbiter', 'run', s) 35 | cls.client = Echo(cls.server_cfg.addresses[0]) 36 | cls.green = EchoGreen(cls.server_cfg.addresses[0]) 37 | cls.msg = b'a'*2**13 38 | cls.pool = greenio.GreenPool() 39 | 40 | async def test_yield_io(self): 41 | result = await self.client(self.msg) 42 | self.assertEqual(result, self.msg) 43 | 44 | @run_in_greenlet 45 | def test_green_io(self): 46 | result = self.green(self.msg) 47 | self.assertEqual(result, self.msg) 48 | 49 | async def test_green_pool(self): 50 | result = await self.pool.submit(self.green, self.msg) 51 | self.assertEqual(result, self.msg) 52 | -------------------------------------------------------------------------------- /tests/bench/test_http_date.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from time import time 3 | 4 | from pulsar.utils.lib import http_date 5 | from wsgiref.handlers import format_date_time 6 | 7 | 8 | class TestPythonCode(unittest.TestCase): 9 | __benchmark__ = True 10 | __number__ = 100000 11 | 12 | def test_http_date_cython(self): 13 | http_date(time()) 14 | 15 | def test_http_date_python(self): 16 | format_date_time(time()) 17 | -------------------------------------------------------------------------------- /tests/bench/test_python.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | string1 = 'a'*20 5 | string2 = 'b'*100 6 | A_STRING = 'this-is-a-string' 7 | 8 | 9 | class TestPythonCode(unittest.TestCase): 10 | __benchmark__ = True 11 | __number__ = 10000 12 | 13 | def test_string_plus(self): 14 | assert len(string1 + string2) == 120 15 | 16 | def test_string_join(self): 17 | assert len(''.join((string1, string2))) == 120 18 | 19 | def test_string_global(self): 20 | assert A_STRING 21 | 22 | def test_string_local(self): 23 | assert 'this-is-a-string' 24 | -------------------------------------------------------------------------------- /tests/bench/test_redis.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | import string 3 | import unittest 4 | 5 | from pulsar.api import HAS_C_EXTENSIONS 6 | from pulsar.apps.ds import redis_parser 7 | 8 | characters = string.ascii_letters + string.digits 9 | 10 | 11 | class RedisPyParser(unittest.TestCase): 12 | __benchmark__ = True 13 | __number__ = 100 14 | _sizes = {'tiny': 2, 15 | 'small': 10, 16 | 'normal': 100, 17 | 'big': 1000, 18 | 'huge': 10000} 19 | redis_py_parser = True 20 | 21 | @classmethod 22 | def setUpClass(cls): 23 | size = cls.cfg.size 24 | nsize = cls._sizes[size] 25 | cls.data = [''.join((choice(characters) for l in range(20))) 26 | for s in range(nsize)] 27 | cls.data_bytes = [(''.join((choice(characters) for l in range(20))) 28 | ).encode('utf-8') for s in range(nsize)] 29 | cls.parser = redis_parser(cls.redis_py_parser)() 30 | cls.chunk = cls.parser.multi_bulk(cls.data) 31 | 32 | def test_pack_command(self): 33 | self.parser.pack_command(self.data) 34 | 35 | def test_encode_multi_bulk(self): 36 | self.parser.multi_bulk(self.data) 37 | 38 | def test_encode_multi_bulk_bytes(self): 39 | self.parser.multi_bulk(self.data_bytes) 40 | 41 | def test_decode_multi_bulk(self): 42 | self.parser.feed(self.chunk) 43 | self.parser.get() 44 | 45 | 46 | @unittest.skipUnless(HAS_C_EXTENSIONS, 'Requires C extensions') 47 | class RedisCParser(RedisPyParser): 48 | redis_py_parser = False 49 | -------------------------------------------------------------------------------- /tests/bench/test_websocket.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | import unittest 3 | 4 | from pulsar.utils.websocket import frame_parser 5 | 6 | 7 | def i2b(args): 8 | return bytes(bytearray(args)) 9 | 10 | 11 | class TestCParser(unittest.TestCase): 12 | __benchmark__ = True 13 | __number__ = 10000 14 | _sizes = {'tiny': 10, 15 | 'small': 200, 16 | 'normal': 2000, 17 | 'large': 10000, 18 | 'huge': 100000} 19 | 20 | @classmethod 21 | def setUpClass(cls): 22 | size = cls.cfg.size 23 | nsize = cls._sizes[size] 24 | cls.data = i2b((randint(0, 255) for v in range(nsize))) 25 | 26 | def setUp(self): 27 | self.server = self.parser() 28 | self.client = self.parser(kind=1) 29 | 30 | def parser(self, kind=0): 31 | return frame_parser(kind=kind) 32 | 33 | def test_masked_encode(self): 34 | self.client.encode(self.data, opcode=2) 35 | 36 | 37 | class TestPyParser(TestCParser): 38 | 39 | def parser(self, kind=0): 40 | return frame_parser(pyparser=True, kind=kind) 41 | -------------------------------------------------------------------------------- /tests/bench/test_wsgi.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.apps.wsgi import WsgiResponse 4 | from pulsar.utils.lib import WsgiResponse as Wsgi 5 | 6 | 7 | common = { 8 | 200: 'OK', 9 | 400: 'Bad Request', 10 | 404: 'Not Found', 11 | 500: 'Internal Server Error' 12 | } 13 | 14 | 15 | class TestWsgi(unittest.TestCase): 16 | __benchmark__ = True 17 | __number__ = 20000 18 | 19 | def setUp(self): 20 | self.environ = {} 21 | 22 | def test_python(self): 23 | WsgiResponse(environ=self.environ, status_code=200) 24 | 25 | def test_cython(self): 26 | Wsgi(environ=self.environ, status_code=200) 27 | 28 | def test_python_content(self): 29 | WsgiResponse(environ=self.environ, status_code=200, 30 | content='this is a test') 31 | 32 | def test_cython_content(self): 33 | Wsgi(environ=self.environ, status_code=200, content='this is a test') 34 | 35 | def test_python_headers(self): 36 | r = WsgiResponse(environ=self.environ, status_code=200, 37 | content='this is a test') 38 | r.get_headers() 39 | 40 | def test_cython_headers(self): 41 | r = Wsgi(environ=self.environ, status_code=200, 42 | content='this is a test') 43 | r.get_headers() 44 | -------------------------------------------------------------------------------- /tests/http/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/tests/http/__init__.py -------------------------------------------------------------------------------- /tests/http/ca_bundle: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICJzCCAZACCQCFmwKKTa6kazANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJH 3 | QjETMBEGA1UECBMKU29tZS1TdGF0ZTEPMA0GA1UEBxMGTG9uZG9uMRIwEAYDVQQK 4 | EwlxdWFudG1pbmQxDzANBgNVBAMTBnB1bHNhcjAeFw0xMzA4MjExNTA5MDhaFw0y 5 | MzA4MTkxNTA5MDhaMFgxCzAJBgNVBAYTAkdCMRMwEQYDVQQIEwpTb21lLVN0YXRl 6 | MQ8wDQYDVQQHEwZMb25kb24xEjAQBgNVBAoTCXF1YW50bWluZDEPMA0GA1UEAxMG 7 | cHVsc2FyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYfp89HNzsnfsBjFh7 8 | I4DrXFbx92wuQwIZB54OtefJPMoxVW+L+eneKwE/KN5QKESQudLmyzFlfdDsnlgc 9 | h4rIFNTt/F4LE1qD3/9XP+jrVaeKWDrcEOTTQxQWt+pfEUOHkJ5Na1tsgdW9nqoD 10 | FPo6PZixBTSFDIRJjezOIY5cbwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAAJw5RQ6 11 | 3roKK/1N32wLu25xNMR8Xmdyuq9dZyGHgrPILqh7MHDoHTCBI4oYAj+0+Ra7KSXI 12 | QjcvfjHY/TIa1di0tcMl1k8lvyCExlmDUc0fGdHeuvgB58Zr9oaSpFgtfY4YmC2L 13 | FbzaVnodId9mn5EL1rplCnhtm9/BKT3MjvAm 14 | -----END CERTIFICATE----- 15 | -----BEGIN RSA PRIVATE KEY----- 16 | MIICXAIBAAKBgQCYfp89HNzsnfsBjFh7I4DrXFbx92wuQwIZB54OtefJPMoxVW+L 17 | +eneKwE/KN5QKESQudLmyzFlfdDsnlgch4rIFNTt/F4LE1qD3/9XP+jrVaeKWDrc 18 | EOTTQxQWt+pfEUOHkJ5Na1tsgdW9nqoDFPo6PZixBTSFDIRJjezOIY5cbwIDAQAB 19 | AoGAKCZWeLmrtSTXHI4+EZXEcLNsNOLm+bssxzhJNihQsZwuxBRxfRI8sAD0oBN7 20 | jPE9NDmovJlNFSKuKk56dnB9ak50udEgT3ez+C5cVAbpTnZ3R+SQJBopU+eNfkeX 21 | DdihfB/x8vM3In2pidVMZqUnDbxGJmq5Zg0hbWrOjolpJZECQQDLGaKzV3fotQt5 22 | ZJGCTyHNl0gIubMBM0dWrp8rdZD7/IXlqH9MSYLu9n++2KwJsEbaKrYLj9J4+SVA 23 | 5cQvfl7pAkEAwDayvabbcuCyzmvaty+/LdzcX3AthcTkYQxRej9IuXcu/HwEBOXD 24 | lL2whEJz4KswerWdUMtdPrt0l2Mm9z65lwJAdGFiPAZZLb3gr1UTlGon4plXq7hN 25 | fNiXfnJdvxeFVv/i8jDVkS9mmewiU4fxPTQHxahH3OQzJSMPV9rRyq1NUQJAMtwG 26 | TZJiDLKR/XaiZ8hVgNAo64PmN7gSae91e7BVEOYNY7d45MbbCndqxoRC3xbM/Bsk 27 | JiW5ZYj6X7hXriJFvwJBALnpD1CGQf1QvZ7qqAzPQ/cyDdy95gf3vc0mDRcm2iD7 28 | wyKKVmc6mzC5s4LVAH5rUH1ZsmYHvj6fZ/+fZxG6s8Q= 29 | -----END RSA PRIVATE KEY----- 30 | -------------------------------------------------------------------------------- /tests/http/req.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | try: 4 | import requests 5 | http = requests.session() 6 | except ImportError: 7 | http = None 8 | 9 | from pulsar.api import HAS_C_EXTENSIONS 10 | from pulsar.apps.test import dont_run_with_thread 11 | 12 | 13 | class TestRequest: 14 | 15 | async def setUp(self): 16 | http = self.client() 17 | response = await http.get(self.httpbin()) 18 | self.assertEqual(str(response), '') 19 | 20 | @dont_run_with_thread 21 | @unittest.skipUnless(http, "requires python requests library") 22 | def test_requests_get_200(self): 23 | if HAS_C_EXTENSIONS and self.tunneling: 24 | raise unittest.SkipTest( 25 | 'requests test when tunneling and C extensions - ticket #287' 26 | ) 27 | response = http.get(self.httpbin(), verify=False, 28 | proxies=self.proxies()) 29 | self.assertEqual(str(response), '') 30 | self.assertEqual(response.status_code, 200) 31 | self.assertTrue(response.content) 32 | self.assertEqual(response.url, self.httpbin()) 33 | response = http.get(self.httpbin('get'), verify=False, 34 | proxies=self.proxies()) 35 | self.assertEqual(response.status_code, 200) 36 | -------------------------------------------------------------------------------- /tests/http/test_bug.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.apps import http 4 | 5 | 6 | class TestBugs(unittest.TestCase): 7 | 8 | async def test_json(self): 9 | c = http.HttpClient() 10 | data = await c.get('https://api.bitfinex.com/v1/pubticker/BTCUSD') 11 | self.assertTrue(data) 12 | -------------------------------------------------------------------------------- /tests/http/test_client.py: -------------------------------------------------------------------------------- 1 | from tests.http import base 2 | 3 | 4 | class TestHttpClient(base.TestHttpClient): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/http/test_proxy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from tests.http import base, req 5 | 6 | 7 | @unittest.skipIf(os.environ.get('CI'), 'Skip on CI see #288') 8 | class TestHttpClientWithProxy(req.TestRequest, base.TestHttpClient): 9 | with_proxy = True 10 | 11 | def _check_server(self, response): 12 | pass 13 | -------------------------------------------------------------------------------- /tests/http/test_tls.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pulsar.apps.http import SSLError, HttpClient 4 | from pulsar.utils.system import platform 5 | 6 | from tests.http import base 7 | 8 | 9 | crt = os.path.join(os.path.dirname(__file__), 'ca_bundle') 10 | 11 | 12 | if platform.type != 'win': 13 | 14 | class TestTlsHttpClient(base.TestHttpClient): 15 | with_tls = True 16 | 17 | async def test_verify(self): 18 | c = HttpClient() 19 | with self.assertRaises(SSLError): 20 | await c.get(self.httpbin()) 21 | response = await c.get(self.httpbin(), verify=False) 22 | self.assertEqual(response.status_code, 200) 23 | response = await c.get(self.httpbin(), verify=crt) 24 | self.assertEqual(response.status_code, 200) 25 | self.assertEqual(response.request.verify, crt) 26 | -------------------------------------------------------------------------------- /tests/http/test_tunnel.py: -------------------------------------------------------------------------------- 1 | from pulsar.utils.system import platform 2 | 3 | from tests.http import base, req 4 | 5 | 6 | if platform.type != 'win': 7 | 8 | class TestHttpsWithProxy(req.TestRequest, base.TestHttpClient): 9 | with_proxy = True 10 | with_tls = True 11 | -------------------------------------------------------------------------------- /tests/stores/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/tests/stores/__init__.py -------------------------------------------------------------------------------- /tests/stores/test_redis.py: -------------------------------------------------------------------------------- 1 | from pulsar.apps.test import check_server, skipUnless 2 | from pulsar.apps.data.redis import RedisScript 3 | 4 | from tests.stores.test_pulsards import unittest, RedisCommands, create_store 5 | from tests.stores.lock import RedisLockTests 6 | from tests.stores.channels import ChannelsTests 7 | 8 | 9 | OK = check_server('redis') 10 | 11 | 12 | @skipUnless(OK, 'Requires a running Redis server') 13 | class TestRedisStore(RedisCommands, 14 | RedisLockTests, 15 | ChannelsTests, 16 | unittest.TestCase): 17 | store = None 18 | 19 | @classmethod 20 | def namespace(cls): 21 | return cls.__name__.lower() 22 | 23 | @classmethod 24 | def setUpClass(cls): 25 | addr = cls.cfg.redis_server 26 | if not addr.startswith('redis://'): 27 | addr = 'redis://%s' % cls.cfg.redis_server 28 | cls.store = create_store(addr, pool_size=3, namespace=cls.namespace()) 29 | cls.client = cls.store.client() 30 | 31 | async def test_script(self): 32 | script = RedisScript("return 1") 33 | self.assertFalse(script.sha) 34 | self.assertTrue(script.script) 35 | result = await script(self.client) 36 | self.assertEqual(result, 1) 37 | self.assertTrue(script.sha) 38 | self.assertTrue(script.sha in self.client.store.loaded_scripts) 39 | result = await script(self.client) 40 | self.assertEqual(result, 1) 41 | 42 | async def test_eval(self): 43 | result = await self.client.eval('return "Hello"') 44 | self.assertEqual(result, b'Hello') 45 | result = await self.client.eval("return {ok='OK'}") 46 | self.assertEqual(result, b'OK') 47 | 48 | async def test_eval_with_keys(self): 49 | result = await self.client.eval("return {KEYS, ARGV}", 50 | ('a', 'b'), 51 | ('first', 'second', 'third')) 52 | self.assertEqual(len(result), 2) 53 | self.assertEqual(result[0], [b'a', b'b']) 54 | self.assertEqual(result[1], [b'first', b'second', b'third']) 55 | -------------------------------------------------------------------------------- /tests/stores/test_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | 4 | from pulsar.apps.ds import redis_to_py_pattern 5 | 6 | 7 | class TestUtils(unittest.TestCase): 8 | 9 | def match(self, c, text): 10 | self.assertEqual(c.match(text).group(), text) 11 | 12 | def not_match(self, c, text): 13 | self.assertEqual(c.match(text), None) 14 | 15 | def test_redis_to_py_pattern(self): 16 | p = redis_to_py_pattern('h?llo') 17 | c = re.compile(p) 18 | self.match(c, 'hello') 19 | self.match(c, 'hallo') 20 | self.not_match(c, 'haallo') 21 | self.not_match(c, 'hallox') 22 | # 23 | p = redis_to_py_pattern('h*llo') 24 | c = re.compile(p) 25 | self.match(c, 'hello') 26 | self.match(c, 'hallo') 27 | self.match(c, 'hasjdbvhckjcvkfcdfllo') 28 | self.not_match(c, 'haallox') 29 | self.not_match(c, 'halloouih') 30 | # 31 | p = redis_to_py_pattern('h[ae]llo') 32 | c = re.compile(p) 33 | self.match(c, 'hello') 34 | self.match(c, 'hallo') 35 | self.not_match(c, 'hollo') 36 | -------------------------------------------------------------------------------- /tests/suite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/pulsar/fee44e871954aa6ca36d00bb5a3739abfdb89b26/tests/suite/__init__.py -------------------------------------------------------------------------------- /tests/suite/test_coverage.py: -------------------------------------------------------------------------------- 1 | '''Test cases for code not covered in standard test cases''' 2 | import tempfile 3 | import shutil 4 | import unittest 5 | 6 | 7 | class PulsarCoverage(unittest.TestCase): 8 | 9 | def test_profile_plugin(self): 10 | from pulsar.apps.test.plugins import profile 11 | p = profile.Profile() 12 | p.config.set('profile', True) 13 | p.configure(p.config) 14 | p.profile_temp_path = tempfile.mkdtemp() 15 | self.assertFalse(p.on_end()) 16 | shutil.rmtree(p.profile_temp_path) 17 | # 18 | lines = list(profile.data_stream(['', 'a b c d e f'])) 19 | self.assertEqual(lines, []) 20 | -------------------------------------------------------------------------------- /tests/suite/test_failures.py: -------------------------------------------------------------------------------- 1 | '''Tests the test suite loader''' 2 | import unittest 3 | 4 | 5 | class TestFailures(unittest.TestCase): 6 | 7 | @unittest.expectedFailure 8 | def test_fail(self): 9 | self.assertEqual(1, 0, "broken") 10 | 11 | 12 | class TestSetupFailure(unittest.TestCase): 13 | 14 | @unittest.expectedFailure 15 | def setUp(self): 16 | self.assertEqual(1, 0, "broken") 17 | 18 | def test_ok(self): 19 | # Never goes in here 20 | pass 21 | 22 | 23 | class TestTearDownFailure(unittest.TestCase): 24 | processed = 0 25 | 26 | @unittest.expectedFailure 27 | def test_fail(self): 28 | self.__class__.processed += 1 29 | self.assertEqual(1, 0, "broken") 30 | 31 | def test_ok(self): 32 | self.__class__.processed += 1 33 | 34 | @classmethod 35 | def tearDownClass(cls): 36 | assert cls.processed == 2, "Should have processed 2" 37 | -------------------------------------------------------------------------------- /tests/suite/test_me.py: -------------------------------------------------------------------------------- 1 | '''Tests the test suite and pulsar distribution.''' 2 | import unittest 3 | import asyncio 4 | 5 | import pulsar 6 | from pulsar.api import send, CommandError, get_actor 7 | from pulsar.apps.test import TestSuite 8 | from pulsar.apps.test.plugins import profile 9 | from pulsar.utils.version import get_version 10 | 11 | 12 | def simple_function(actor): 13 | return actor.name 14 | 15 | 16 | async def wait(actor, period=0.5): 17 | start = actor._loop.time() 18 | await asyncio.sleep(period) 19 | return actor._loop.time() - start 20 | 21 | 22 | class TestTestWorker(unittest.TestCase): 23 | 24 | def test_TestSuiteMonitor(self): 25 | arbiter = get_actor() 26 | self.assertTrue(len(arbiter.monitors) >= 1) 27 | monitor = arbiter.registered['test'] 28 | app = monitor.app 29 | self.assertTrue(isinstance(app, TestSuite)) 30 | 31 | async def test_unknown_send_target(self): 32 | # The target does not exists 33 | with self.assertRaises(CommandError): 34 | await send('vcghdvchdgcvshcd', 'ping') 35 | 36 | async def test_multiple_execute(self): 37 | m = await asyncio.gather( 38 | send('arbiter', 'run', wait, 1.2), 39 | send('arbiter', 'ping'), 40 | send('arbiter', 'echo', 'ciao!'), 41 | send('arbiter', 'run', wait, 2.1), 42 | send('arbiter', 'echo', 'ciao again!') 43 | ) 44 | self.assertTrue(m[0] >= 1.1) 45 | self.assertEqual(m[1], 'pong') 46 | self.assertEqual(m[2], 'ciao!') 47 | self.assertTrue(m[3] >= 2.0) 48 | self.assertEqual(m[4], 'ciao again!') 49 | 50 | def test_no_plugins(self): 51 | suite = TestSuite(test_plugins=[]) 52 | self.assertFalse(suite.cfg.test_plugins) 53 | self.assertFalse('profile' in suite.cfg.settings) 54 | 55 | def test_profile_plugins(self): 56 | suite = TestSuite(plugins=[profile.Profile()]) 57 | self.assertTrue(suite.cfg.plugins) 58 | self.assertTrue('profile' in suite.cfg.settings) 59 | self.assertTrue('profile_stats_path' in suite.cfg.settings) 60 | 61 | def test_version(self): 62 | self.assertTrue(pulsar.VERSION) 63 | self.assertTrue(pulsar.__version__) 64 | self.assertEqual(pulsar.__version__, get_version(pulsar.VERSION)) 65 | self.assertTrue(len(pulsar.VERSION) >= 2) 66 | 67 | def test_meta(self): 68 | for m in ("__version__", "__doc__"): 69 | self.assertTrue(getattr(pulsar, m, None)) 70 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from pulsar.api import Config 2 | 3 | 4 | def connection_made(conn): 5 | return conn 6 | 7 | 8 | def post_fork(actor): 9 | return actor 10 | 11 | 12 | def config(**kw): 13 | return Config(**kw) 14 | 15 | 16 | foo = 5 17 | -------------------------------------------------------------------------------- /tests/utils/structures/test_structures.py: -------------------------------------------------------------------------------- 1 | '''Tests the tools and utilities in pulsar.utils.''' 2 | import unittest 3 | import pickle 4 | 5 | from pulsar.utils.structures import AttributeDictionary 6 | 7 | 8 | class TestAttributeDictionary(unittest.TestCase): 9 | 10 | def testInit(self): 11 | self.assertRaises(TypeError, AttributeDictionary, {}, {}) 12 | a = AttributeDictionary({'bla': 1}, foo='pippo') 13 | self.assertEqual(dict(a), {'bla': 1, 'foo': 'pippo'}) 14 | self.assertEqual(len(a), 2) 15 | 16 | def testAssign(self): 17 | a = AttributeDictionary() 18 | a['ciao'] = 5 19 | self.assertEqual(a.ciao, 5) 20 | self.assertEqual(a['ciao'], 5) 21 | self.assertEqual(list(a.values()), [5]) 22 | self.assertEqual(list(a.items()), [('ciao', 5)]) 23 | 24 | def testCopy(self): 25 | a = AttributeDictionary(foo=5, bla='ciao') 26 | self.assertEqual(len(a), 2) 27 | b = a.copy() 28 | self.assertEqual(a, b) 29 | self.assertNotEqual(id(a), id(b)) 30 | 31 | def __test_pickle(self): 32 | # TODO: this fails at times 33 | a = AttributeDictionary() 34 | a['ciao'] = 5 35 | b = pickle.dumps(a) 36 | c = pickle.loads(b) 37 | self.assertEqual(a, c) 38 | -------------------------------------------------------------------------------- /tests/utils/test_autoreload.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import skipUnless 3 | 4 | from pulsar.utils import autoreload 5 | 6 | 7 | class AutoReloadTest(unittest.TestCase): 8 | 9 | @skipUnless('watchdog' in autoreload.reloaders, 'Requires watchdog') 10 | def test_watchdog(self): 11 | self.assertEqual(autoreload.reloaders['auto'], 12 | autoreload.reloaders['watchdog']) 13 | reloader = autoreload.reloaders['auto']() 14 | self.assertEqual(reloader.interval, 1) 15 | -------------------------------------------------------------------------------- /tests/utils/test_context.py: -------------------------------------------------------------------------------- 1 | """Tests task context 2 | """ 3 | import unittest 4 | import asyncio 5 | 6 | from pulsar.api import context 7 | 8 | 9 | class TestContextEmpty(unittest.TestCase): 10 | 11 | def test_get(self): 12 | self.assertEqual(context.get('foo'), None) 13 | 14 | def test_stack_get(self): 15 | self.assertEqual(context.stack_get('foo'), None) 16 | 17 | 18 | class TestContext(unittest.TestCase): 19 | 20 | @classmethod 21 | def setUpClass(cls): 22 | context.setup() 23 | 24 | @classmethod 25 | def tearDownClass(cls): 26 | context.remove() 27 | 28 | def test_task_factory(self): 29 | self.assertEqual(asyncio.get_event_loop().get_task_factory(), context) 30 | 31 | async def test_set_get_pop(self): 32 | context.set('foo', 5) 33 | self.assertEqual(context.get('foo'), 5) 34 | self.assertEqual(context.pop('foo'), 5) 35 | self.assertEqual(context.get('foo'), None) 36 | 37 | async def test_set_get_pop_nested(self): 38 | context.set('foo', 5) 39 | self.assertEqual(context.get('foo'), 5) 40 | await asyncio.get_event_loop().create_task(self.nested()) 41 | self.assertEqual(context.get('foo'), 5) 42 | self.assertEqual(context.get('bla'), None) 43 | 44 | async def test_stack(self): 45 | with context.begin(text='ciao', planet='mars'): 46 | self.assertEqual(context.stack_get('text'), 'ciao') 47 | self.assertEqual(context.stack_get('planet'), 'mars') 48 | self.assertEqual(context.stack_get('text'), None) 49 | self.assertEqual(context.stack_get('planet'), None) 50 | 51 | def test_typeerror(self): 52 | with self.assertRaises(TypeError): 53 | with context.begin(1, 2): 54 | pass 55 | 56 | async def nested(self): 57 | context.set('bla', 7) 58 | self.assertEqual(context.get('bla'), 7) 59 | self.assertEqual(context.get('foo'), 5) 60 | context.set('foo', 8) 61 | self.assertEqual(context.get('foo'), 8) 62 | -------------------------------------------------------------------------------- /tests/utils/test_ext.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from pulsar import api 5 | from pulsar.utils.system import platform 6 | 7 | 8 | @unittest.skipIf(os.environ.get('PULSARPY') == 'yes', 'Pulsar extensions') 9 | class TestApi(unittest.TestCase): 10 | 11 | def test_has(self): 12 | try: 13 | self.assertTrue(api.HAS_C_EXTENSIONS) 14 | except Exception: 15 | if not platform.is_windows: 16 | raise 17 | -------------------------------------------------------------------------------- /tests/utils/test_git.py: -------------------------------------------------------------------------------- 1 | '''Tests git info.''' 2 | import unittest 3 | 4 | from pulsar.utils.version import gitrepo 5 | 6 | 7 | class TestGit(unittest.TestCase): 8 | 9 | def test_pulsar(self): 10 | info = gitrepo() 11 | self.assertTrue(info) 12 | self.assertTrue(info['branch']) 13 | self.assertIsInstance(info['head'], dict) 14 | self.assertIsInstance(info['remotes'], list) 15 | remote = info['remotes'][0] 16 | self.assertIsInstance(remote, dict) 17 | self.assertEqual(remote['name'], 'origin') 18 | -------------------------------------------------------------------------------- /tests/utils/test_misc.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from pulsar.utils.exceptions import reraise 5 | 6 | 7 | class TestMiscellaneous(unittest.TestCase): 8 | 9 | def test_reraise(self): 10 | self.assertRaises(RuntimeError, reraise, RuntimeError, RuntimeError()) 11 | try: 12 | raise RuntimeError('bla') 13 | except Exception: 14 | exc_info = sys.exc_info() 15 | self.assertRaises(RuntimeError, reraise, *exc_info) 16 | -------------------------------------------------------------------------------- /tests/utils/test_mixins.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestMixins(unittest.TestCase): 5 | 6 | def testLocal(self): 7 | from pulsar.utils.structures import AttributeDictionary 8 | from pulsar.utils.log import LocalMixin 9 | elem = LocalMixin() 10 | el = elem.local 11 | self.assertTrue(isinstance(el, AttributeDictionary)) 12 | self.assertEqual(id(elem.local), id(el)) 13 | self.assertEqual(elem.local.process, None) 14 | elem.local.process = True 15 | self.assertEqual(elem.local.process, True) 16 | -------------------------------------------------------------------------------- /tests/utils/test_path.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.utils.path import Path 4 | 5 | 6 | class TestPath(unittest.TestCase): 7 | 8 | def testThis(self): 9 | p = Path(__file__) 10 | self.assertTrue(p.isfile()) 11 | self.assertFalse(p.isdir()) 12 | c = Path.cwd() 13 | self.assertNotEqual(p, c) 14 | self.assertTrue(c.isdir()) 15 | 16 | def testDir(self): 17 | c = Path.cwd() 18 | self.assertEqual(c, c.dir()) 19 | c = Path('/sdjc/scdskjcdnsd/dhjdhjdjksdjksdksd') 20 | self.assertFalse(c.exists()) 21 | self.assertRaises(ValueError, c.dir) 22 | 23 | def testAdd2Python(self): 24 | p = Path('/sdjc/scdskjcdnsd/dhjdhjdjksdjksdksd') 25 | module = p.add2python('pulsar') 26 | self.assertEqual(module.__name__, 'pulsar') 27 | self.assertRaises(ValueError, p.add2python, 'kaputttt') 28 | 29 | def testAdd2Python_failure(self): 30 | p = Path() 31 | self.assertRaises(ImportError, p.add2python, 'kaputttt') 32 | self.assertFalse(p.add2python('vdfvdavfdv', must_exist=False)) 33 | -------------------------------------------------------------------------------- /tests/utils/test_pid.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from pulsar.api import send 5 | from pulsar.apps.test import ActorTestMixin 6 | from pulsar.utils.tools import Pidfile 7 | from pulsar.utils.system import platform 8 | 9 | 10 | @unittest.skipUnless(platform.type != 'win', 'This fails in windows') 11 | class TestPidfile(ActorTestMixin, unittest.TestCase): 12 | concurrency = 'process' 13 | 14 | async def test_create_pid(self): 15 | proxy = await self.spawn_actor(name='pippo') 16 | info = await send(proxy, 'info') 17 | result = info['actor'] 18 | self.assertTrue(result['is_process']) 19 | pid = result['process_id'] 20 | # 21 | p = Pidfile() 22 | self.assertEqual(p.fname, None) 23 | self.assertEqual(p.pid, None) 24 | p.create(pid) 25 | self.assertTrue(p.fname) 26 | self.assertEqual(p.pid, pid) 27 | self.assertTrue(p.exists) 28 | # 29 | p1 = Pidfile(p.fname) 30 | self.assertRaises(RuntimeError, p1.create, p.pid+1) 31 | # 32 | p1 = Pidfile('bla/ksdcskcbnskcdbskcbksdjcb') 33 | self.assertRaises(RuntimeError, p1.create, p.pid+1) 34 | p1.unlink() 35 | p.unlink() 36 | self.assertFalse(os.path.exists(p.fname)) 37 | 38 | def test_stale_pid(self): 39 | p = Pidfile() 40 | p.create(798797) 41 | self.assertTrue(p.fname) 42 | self.assertEqual(p.pid, 798797) 43 | self.assertFalse(p.exists) 44 | # 45 | # Now create again with different pid 46 | p.create(798798) 47 | self.assertEqual(p.pid, 798798) 48 | self.assertFalse(p.exists) 49 | p.unlink() 50 | -------------------------------------------------------------------------------- /tests/utils/test_slugify.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from unittest import skipUnless 5 | 6 | from pulsar.utils.slugify import slugify, unidecode 7 | 8 | 9 | @skipUnless(unidecode, 'Requires unidecode package') 10 | class TestSlugify(unittest.TestCase): 11 | 12 | def test_manager(self): 13 | 14 | txt = "This is a test ---" 15 | r = slugify(txt) 16 | self.assertEqual(r, "this-is-a-test") 17 | 18 | txt = "This -- is a ## test ---" 19 | r = slugify(txt) 20 | self.assertEqual(r, "this-is-a-test") 21 | 22 | txt = 'C\'est déjà l\'été.' 23 | r = slugify(txt) 24 | self.assertEqual(r, "cest-deja-lete") 25 | 26 | txt = 'Nín hǎo. Wǒ shì zhōng guó rén' 27 | r = slugify(txt) 28 | self.assertEqual(r, "nin-hao-wo-shi-zhong-guo-ren") 29 | 30 | txt = 'Компьютер' 31 | r = slugify(txt) 32 | self.assertEqual(r, "kompiuter") 33 | 34 | txt = 'jaja---lol-méméméoo--a' 35 | r = slugify(txt) 36 | self.assertEqual(r, "jaja-lol-mememeoo-a") 37 | 38 | txt = 'jaja---lol-méméméoo--a' 39 | r = slugify(txt, max_length=9) 40 | self.assertEqual(r, "jaja-lol") 41 | 42 | txt = 'jaja---lol-méméméoo--a' 43 | r = slugify(txt, max_length=15) 44 | self.assertEqual(r, "jaja-lol-mememe") 45 | 46 | txt = 'jaja---lol-méméméoo--a' 47 | r = slugify(txt, max_length=50) 48 | self.assertEqual(r, "jaja-lol-mememeoo-a") 49 | 50 | txt = 'jaja---lol-méméméoo--a' 51 | r = slugify(txt, max_length=15, word_boundary=True) 52 | self.assertEqual(r, "jaja-lol-a") 53 | 54 | txt = 'jaja---lol-méméméoo--a' 55 | r = slugify(txt, max_length=19, word_boundary=True) 56 | self.assertEqual(r, "jaja-lol-mememeoo") 57 | 58 | txt = 'jaja---lol-méméméoo--a' 59 | r = slugify(txt, max_length=20, word_boundary=True) 60 | self.assertEqual(r, "jaja-lol-mememeoo-a") 61 | 62 | txt = 'jaja---lol-méméméoo--a' 63 | r = slugify(txt, max_length=20, word_boundary=True, separator=".") 64 | self.assertEqual(r, "jaja.lol.mememeoo.a") 65 | 66 | txt = 'jaja---lol-méméméoo--a' 67 | r = slugify(txt, max_length=20, word_boundary=True, separator="ZZZZZZ") 68 | self.assertEqual(r, "jajaZZZZZZlolZZZZZZmememeooZZZZZZa") 69 | -------------------------------------------------------------------------------- /tests/utils/test_string.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.utils import security 4 | from pulsar.utils.string import random_string 5 | 6 | 7 | class TestSecurity(unittest.TestCase): 8 | 9 | def testSalt(self): 10 | s1 = security.gen_salt(10) 11 | self.assertEqual(len(s1), 10) 12 | self.assertNotEqual(security.gen_salt(10), s1) 13 | s1 = security.gen_salt(30) 14 | self.assertEqual(len(s1), 30) 15 | self.assertRaises(ValueError, security.gen_salt, 0) 16 | 17 | def testPassword(self): 18 | password = 'my-test$$-password' 19 | hash = security.generate_password_hash(password) 20 | self.assertTrue('$' in hash) 21 | self.assertFalse(security.check_password_hash('bla', 'bla')) 22 | self.assertFalse(security.check_password_hash(hash, 'bla')) 23 | self.assertFalse(security.check_password_hash(hash, 'bla$foo')) 24 | self.assertTrue(security.check_password_hash(hash, password)) 25 | 26 | def test_random_string(self): 27 | s1 = random_string(min_len=20) 28 | self.assertEqual(len(s1), 20) 29 | self.assertIsInstance(s1, str) 30 | self.assertNotEqual(s1, random_string(min_len=20)) 31 | self.assertNotEqual(s1, random_string(min_len=20)) 32 | self.assertNotEqual(s1, random_string(min_len=20)) 33 | -------------------------------------------------------------------------------- /tests/utils/test_system.py: -------------------------------------------------------------------------------- 1 | '''Tests the tools and utilities in pulsar.utils.''' 2 | import unittest 3 | 4 | from pulsar.utils.system import platform, get_maxfd 5 | 6 | 7 | class TestSystem(unittest.TestCase): 8 | 9 | @unittest.skipUnless(platform.is_posix, 'Posix platform required') 10 | def testPlatform(self): 11 | self.assertFalse(platform.is_windows) 12 | 13 | def test_maxfd(self): 14 | m = get_maxfd() 15 | self.assertTrue(m) 16 | -------------------------------------------------------------------------------- /tests/utils/test_text.py: -------------------------------------------------------------------------------- 1 | '''Tests the tools and utilities in pulsar.utils.''' 2 | import unittest 3 | 4 | from pulsar.utils.log import lazy_string 5 | 6 | 7 | @lazy_string 8 | def blabla(n): 9 | return 'AAAAAAAAAAAAAAAAAAAA %s' % n 10 | 11 | 12 | class TestTextUtils(unittest.TestCase): 13 | 14 | def testLazy(self): 15 | r = blabla(3) 16 | self.assertEqual(r.value, None) 17 | v = str(r) 18 | self.assertEqual(v, 'AAAAAAAAAAAAAAAAAAAA 3') 19 | self.assertEqual(r.value, v) 20 | -------------------------------------------------------------------------------- /tests/wsgi/ab.txt: -------------------------------------------------------------------------------- 1 | ab -n 5000 -c 8 http://127.0.0.1:8060/ 2 | 3 | ab -k -n 5000 -c 8 http://127.0.0.1:8060/ -------------------------------------------------------------------------------- /tests/wsgi/test_accept.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.apps.test import test_wsgi_request 4 | 5 | 6 | class AcceptTests(unittest.TestCase): 7 | 8 | async def test_empty_mime(self): 9 | request = await test_wsgi_request(headers=[('Accept', None)]) 10 | content_types = request.content_types 11 | self.assertFalse(content_types) 12 | 13 | async def test_content_types(self): 14 | request = await test_wsgi_request(headers=[ 15 | ('Accept', 'text/html, application/xhtml+xml, ' 16 | 'application/xml;q=0.9, */*;q=0.8')]) 17 | content_types = request.content_types 18 | self.assertTrue(content_types) 19 | self.assertEqual(len(content_types), 4) 20 | self.assertEqual(id(content_types), id(request.content_types)) 21 | self.assertTrue('text/html' in content_types) 22 | self.assertTrue('text/plain' in content_types) 23 | self.assertEqual(content_types.quality('text/html'), 1) 24 | self.assertEqual(content_types.quality('text/plain'), 0.8) 25 | self.assertEqual(content_types.quality('application/json'), 0.8) 26 | self.assertEqual(content_types.best, 'text/html') 27 | 28 | async def test_best(self): 29 | request = await test_wsgi_request(headers=[ 30 | ('Accept', 'text/html, application/xhtml+xml, ' 31 | 'application/xml;q=0.9, */*;q=0.8')]) 32 | content_types = request.content_types 33 | self.assertTrue(content_types) 34 | self.assertEqual(content_types.best_match(('text/html', 35 | 'application/json')), 36 | 'text/html') 37 | self.assertEqual(content_types.best_match(('application/json', 38 | 'text/html')), 39 | 'text/html') 40 | -------------------------------------------------------------------------------- /tests/wsgi/test_errors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pulsar.api import HttpRedirect 4 | 5 | 6 | class WsgiRequestTests(unittest.TestCase): 7 | 8 | def test_httpredirect(self): 9 | location = 'https://pythonhosted.org/pulsar' 10 | r = HttpRedirect(location) 11 | self.assertEqual(r.location, location) 12 | r = HttpRedirect(location, headers=[('Content-Type', 'text/html')]) 13 | self.assertEqual(len(r.headers), 2) 14 | self.assertEqual(r.location, location) 15 | --------------------------------------------------------------------------------