Merge branch 'release/0.19.0'

This commit is contained in:
Tyson Clugg 2015-12-16 22:40:58 +11:00
commit db1a5b09cb
42 changed files with 1044 additions and 305 deletions

View file

@ -1,3 +0,0 @@
[run]
branch=True
source=dddp

23
.gitignore vendored
View file

@ -2,21 +2,30 @@
# $GIT_DIR/info/exclude or the core.excludesFile configuration variable as
# described in https://git-scm.com/docs/gitignore
*.egg-info
# python
*.pot
*.py[co]
__pycache__
# distutils/setuptools
.eggs/
*.egg-info
MANIFEST
dist/
build/
# docs
docs/_build/
docs/locale/
node_modules/
tests/coverage_html/
tests/.coverage
build/
tests/report/
docs/node_modules/
# test suite
.cache/
.coverage
.eggs/
.tox/
htmlcov/
tests/report/
# meteor
dddp/test/build/
dddp/test/meteor_todos/.meteor/

29
.travis.yml Normal file
View file

@ -0,0 +1,29 @@
# .travis.yml automatically generated by ".travis.yml.sh"
# Container-based builds used if "sudo: false" --> fast boot (1-6s)
# https://docs.travis-ci.com/user/ci-environment/
sudo: false
language: python
env:
- TOXENV="py27-django1.8"
- TOXENV="py27-django1.9"
- TOXENV="py33-django1.8"
- TOXENV="py34-django1.8"
- TOXENV="py34-django1.9"
- TOXENV="py35-django1.8"
- TOXENV="py35-django1.9"
- TOXENV="pypy3-django1.8"
- TOXENV="pypy3-django1.9"
- TOXENV="pypy-django1.8"
- TOXENV="pypy-django1.9"
install:
- pip install tox coveralls
script:
- tox
after_success:
coveralls

22
.travis.yml.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
cat<<EOF
# .travis.yml automatically generated by "$0"
# Container-based builds used if "sudo: false" --> fast boot (1-6s)
# https://docs.travis-ci.com/user/ci-environment/
sudo: false
language: python
env:
$( tox -l | grep '^py' | sort -n | sed -e 's/^.*$/ - TOXENV="\0"/' )
install:
- pip install tox coveralls
script:
- tox
after_success:
coveralls
EOF

View file

@ -1,34 +1,67 @@
Change Log
==========
0.18.1
All notable changes to this project will be documented in this file.
This project adheres to `Semantic Versioning <http://semver.org/>`_.
0.19.1
------
* Dropped support for Django 1.7 (support expired on December 1 2015,
see https://www.djangoproject.com/download/#supported-versions).
* Require `setuptools>=18.5` at install time due to use of
`python_platform_implementation` environment marker.
* Moved repository to https://github.com/django-ddp/django-ddp (new
Github organisation).
* Started work on documentation refresh.
* Fail on start if any child threads can't start (eg: port in use).
* Rudimentary support for dynamic publications which may now refer to `this.user`.
* Correctly close DB connections during shutdown, useful for test suite.
* Add missing versions and dates to the change log, and note on Semantic
Versioning.
* Emit `django.core.signals.request_finished` to close DB connection and
yield to other greenlets between processing messages from WebSocket.
* Back to universal wheels, thanks to PEP-0496 environment markers.
* Expand test suite coverage to start a DDP server instance and perform
a HTTP GET request, no WebSocket/DDP tests yet. Fails in Pyton 3
because of a bug in `gevent-websocket`
(https://bitbucket.org/noppo/gevent-websocket/issues/54/python-3-support).
* Fix for #23 (Python 3 compatibility).
* Set `application_name` on PostgreSQL async connection.
* Send `django.core.signals.request_finished` when closing WebSocket.
* Don't require `DJANGO_SETTINGS_MODULE` to import API.
* Tox test suite updated to runs against Python 2.7/3.3/3.4/3.5 and
Django 1.8/1.9.
* Build wheels from tox environment to ensure consistency.
* Remove `debug` argment from `PostgresGreenlet` class (unused).
0.18.1 (2015-11-06)
-------------------
* Don't assume Django projects include a `wsgi.py`.
0.18.0
------
* Python implementaiton specific builds using tox so that Python2 and
0.18.0 (2015-11-05)
-------------------
* Python implementation specific builds using tox so that Python2 and
Python3 can have different dependencies (eg: gevent>=1.1 for Pyton3).
* Added support for `METEOR_SETTINGS` environment variable.
0.17.3
------
0.17.3 (2015-10-30)
-------------------
* Depend on gevent>=1.1b6 if running anything other than CPython 2.7,
otherwise allow gevent 1.0 (current stable).
* Preliminary (but broken) support for PyPy/Jython/IronPython though
platform specific install_requires on psycopg2cffi instead of psycopg2
for all platforms except CPython 2/3.
0.17.2
------
0.17.2 (2015-10-29)
-------------------
* Python 3 fixes using `six` compatibility library (#16, #17).
0.17.1
------
0.17.1 (2015-10-14)
-------------------
* Fix minor issue where some subscription queries still used slow queries.
0.17.0
------
0.17.0 (2015-10-14)
-------------------
* Make the SQL for subscriptions much faster for PostgreSQL.
* Repeatable builds using ye olde make.
* Use tox test runner - no tests yet (#11).
@ -36,15 +69,15 @@ Change Log
* Started documentation using Sphinx (#10).
* Python 3 style exception handling.
0.16.0
------
0.16.0 (2015-10-13)
-------------------
* New setting: `DDP_API_ENDPOINT_DECORATORS`.
This setting takes a list of dotted import paths to decorators which are applied to API endpoints. For example, enable New Relic instrumentation by adding the line below to your Django `settings.py`:
.. code:: python
DDP_API_ENDPOINT_DECORATORS = ['newrelic.agent.background_task']
* Fixed #7 -- Warn if using DB engines other than psycopg2 - thanks @Matvey-Kuk.
* Improvements to error/exception handling.
* Warn if many TX chunks are queued in case WebSocket has stalled.
@ -53,15 +86,15 @@ Change Log
* Work towards #16 -- Use `psycopg2cffi` compatibility if `psycopg2` not
installed.
0.15.0
------
0.15.0 (2015-09-25)
-------------------
* Renamed `Logs` collection and publication to `dddp.logs` to be consistent with naming conventions used elsewhere.
* Pass all attributes from `logging.LogRecord` via `dddp.logs` collection.
* Use select_related() and resultant cached relational fields to speed up Colleciton.serialize() by significantly reducing round-trips to the database.
* Fix bug in `get_meteor_ids()` which caused many extra database hits.
0.14.0
------
0.14.0 (2015-09-22)
-------------------
* Correctly handle serving app content from the root path of a domain.
* Account security tokens are now calculated for each minute allowing for finer grained token expiry.
* Fix bug in error handling where invalid arguments were being passed to `logging.error()`.
@ -74,8 +107,8 @@ Change Log
* Honour `--verbosity` in `dddp` command, now showing API endpoints in more verbose modes.
* Updated `dddp.test` to Meteor 1.2 and also showing example of URL config to serve Meteor files from Python.
0.13.0
------
0.13.0 (2015-09-18)
-------------------
* Abstract DDPLauncher out from dddp.main.serve to permit use from other contexts.
* Allow Ctrl-C (Break) handling at any time.
* Only run async DB connection when PostgresGreenlet is running.
@ -87,13 +120,13 @@ Change Log
* Use sane default options for `python setup.py bdist_wheel`.
* Fixed README link to meteor - thanks @LegoStormtroopr.
0.12.2
------
0.12.2 (2015-08-27)
-------------------
* Set blank=True on AleaIdField, allowing adding items without inventing
IDs yourself.
0.12.1
------
0.12.1 (2015-08-13)
-------------------
* Add `AleaIdMixin` which provides `aid = AleaIdField(unique=True)` to
models.
* Use `AleaIdField(unique=True)` wherever possible when translating
@ -101,102 +134,102 @@ Change Log
round trips to the database and hence drastically improving
performance when such fields are available.
0.12.0
------
0.12.0 (2015-08-11)
-------------------
* Get path to `star.json` from view config (defined in your urls.py)
instead of from settings.
* Dropped `dddp.server.views`, use `dddp.views` instead.
0.11.0
------
0.11.0 (2015-08-10)
-------------------
* Support more than 8KB of change data by splitting large payloads into
multiple chunks.
0.10.2
------
0.10.2 (2015-08-10)
-------------------
* Add `Logs` publication that can be configured to emit logs via DDP
through the use of the `dddp.logging.DDPHandler` log handler.
* Add option to dddp daemon to provide a BackdoorServer (telnet) for
interactive debugging (REPL) at runtime.
0.10.1
------
0.10.1 (2015-07-28)
-------------------
* Bugfix dddp.accounts forgot_password feature.
0.10.0
------
0.10.0 (2015-07-21)
-------------------
* Stop processing request middleware upon connection - see
https://github.com/commoncode/django-ddp/commit/e7b38b89db5c4e252ac37566f626b5e9e1651a29
for rationale. Access to `this.request.user` is gone.
* Add `this.user` handling to dddp.accounts.
0.9.14
------
0.9.14 (2015-07-18)
-------------------
* Fix ordering of user added vs login ready in dddp.accounts
authentication methods.
0.9.13
------
0.9.13 (2015-07-17)
-------------------
* Add dddp.models.get_object_ids helper function.
* Add ObjectMappingMixini abstract model mixin providing
GenericRelation back to ObjectMapping model.
0.9.12
------
0.9.12 (2015-07-16)
-------------------
* Bugfix /app.model/schema helper method on collections to work with
more model field types.
0.9.11
------
0.9.11 (2015-07-14)
-------------------
* Fix bug in post login/logout subscription handling.
0.9.10
------
0.9.10 (2015-07-08)
-------------------
* Fix bug in Accounts.forgotPassword implementation.
0.9.9
-----
0.9.9 (2015-07-08)
------------------
* Match return values for Accounts.changePassword and
Accounts.changePassword methods in dddp.accounts submodule.
0.9.8
-----
0.9.8 (2015-07-08)
------------------
* Fix method signature for Accouts.changePassword.
0.9.7
-----
0.9.7 (2015-07-08)
------------------
* Updated Accounts hashing to prevent cross-purposing auth tokens.
0.9.6
-----
0.9.6 (2015-07-07)
------------------
* Correct method signature to match Meteor Accounts.resetPassword in
dddp.accounts submodule.
0.9.5
-----
0.9.5 (2015-07-03)
------------------
* Include array of `permissions` on User publication.
0.9.4
-----
0.9.4 (2015-06-29)
------------------
* Use mimetypes module to correctly guess mime types for Meteor files
being served.
0.9.3
-----
0.9.3 (2015-06-29)
------------------
* Include ROOT_URL_PATH_PREFIX in ROOT_URL when serving Meteor build
files.
0.9.2
-----
0.9.2 (2015-06-22)
------------------
* Use HTTPS for DDP URL if settings.SECURE_SSL_REDIRECT is set.
0.9.1
-----
0.9.1 (2015-06-16)
------------------
* Added support for django.contrib.postres.fields.ArrayField
serialization.
0.9.0
-----
0.9.0 (2015-06-14)
------------------
* Added Django 1.8 compatibility. The current implementation has a
hackish (but functional) implementation to use PostgreSQL's
`array_agg` function. Pull requests are welcome.
@ -204,31 +237,31 @@ Change Log
`dbarray` package for this even though not strictly required with
Django 1.8. Once again, pull requests are welcome.
0.8.1
-----
0.8.1 (2015-06-10)
------------------
* Add missing dependency on `pybars3` used to render boilerplate HTML
template when serving Meteor application files.
0.8.0
-----
0.8.0 (2015-06-09)
------------------
* Add `dddp.server` Django app to serve Meteor application files.
* Show input params after traceback if exception occurs in API methods.
* Small pylint cleanups.
0.7.0
-----
0.7.0 (2015-05-28)
------------------
* Refactor serialization to improve performance through reduced number
of database queries, especially on sub/unsub.
* Fix login/logout user subscription, now emitting user `added`/
`removed` upon `login`/`logout` respectively.
0.6.5
-----
0.6.5 (2015-05-27)
------------------
* Use OrderedDict for geventwebsocket.Resource spec to support
geventwebsockets 0.9.4 and above.
0.6.4
-----
0.6.4 (2015-05-27)
------------------
* Send `removed` messages when client unsubscribes from publications.
* Add support for SSL options and --settings=SETTINGS args in dddp tool.
* Add `optional` and `label` attributes to ManyToManyField simple
@ -237,17 +270,17 @@ Change Log
than when queuing messages.
* Move test projects into path that can be imported post install.
0.6.3
-----
0.6.3 (2015-05-21)
------------------
* Refactor pub/sub functionality to fix support for `removed` messages.
0.6.2
-----
0.6.2 (2015-05-20)
------------------
* Bugfix issue where DDP connection thread stops sending messages after
changing item that has subscribers for other connections but not self.
0.6.1
-----
0.6.1 (2015-05-18)
------------------
* Fix `createUser` method to login new user after creation.
* Dump stack trace to console on error for easier debugging DDP apps.
* Fix handing of F expressions in object change handler.
@ -255,14 +288,14 @@ Change Log
* Per connection tracking of sent objects so changed/added sent
appropriately.
0.6.0
-----
0.6.0 (2015-05-12)
------------------
* Add dddp.accounts module which provides password based auth mapping to
django.contrib.auth module.
* Fix ordering of change messages and result message in method calls.
0.5.0
-----
0.5.0 (2015-05-07)
------------------
* Drop relations to sessions.Session as WebSocket requests don't have
HTTP cookie support -- **you must `migrate` your database after
upgrading**.
@ -275,8 +308,8 @@ Change Log
* Cleanup transaction handling to apply once at the entry point for DDP
API calls.
0.4.0
-----
0.4.0 (2015-04-28)
------------------
* Make live updates honour user_rel restrictions, also allow superusers
to see everything.
* Support serializing objects that are saved with F expressions by
@ -288,8 +321,8 @@ Change Log
have user_rel items defined). This change includes a schema change,
remember to run migrations after updating.
0.3.0
-----
0.3.0 (2015-04-23)
------------------
* New DB field: Connection.server_addr -- **you must `migrate` your
database after upgrading**.
* Cleanup connections on shutdown (and purge associated subscriptions).
@ -300,15 +333,64 @@ Change Log
* Fix `unsubscribe` from publications.
* Fix `/schema` method call.
0.2.5
-----
0.2.5 (2015-04-25)
------------------
* Fix foreign key references in change messages to correctly reference
related object rather than source object.
0.2.4
-----
0.2.4 (2015-04-15)
------------------
* Fix unicode rendering bug in DDP admin for ObjectMapping model.
0.2.3
-----
0.2.3 (2015-04-15)
------------------
* Add `dddp` console script to start DDP service in more robust manner than using the dddp Django mangement command.
0.2.2 (2015-04-14)
------------------
* Don't include null/None reply from method calls in message.
* Force creation of Alea/Meteor ID even if nobody seems to care -- they
do care if they're using the ID with latency compensated views.
* Support collections to models having non-integer primary key fields.
* Fix latency compensated Alea/Meteor ID generation to match Meteor
semantics of using a namespace to generate seeded Alea PRNGs.
0.2.1 (2015-04-10)
------------------
* Change validation so that we now pass the DDP test suite
<http://ddptest.meteor.com/>.
* Add lots of useful info to the README.
0.2.0 (2015-04-08)
------------------
* Add `dddp.models.get_meteor_id` and `dddp.models.get_object_id`
methods.
* Add `Connection`, `Subscription` and `SubscriptionColleciton` models,
instances of which are managed during life cycle of connections and
subscriptions.
* Fixed incorrect use of `django.core.serializers` where different
threads used same the serializer instance.
* Add `Collection.user_rel` class attribute allowing user-specific
filtering of objects at the collection level.
* Add `dddp.test` test project with example meteor-todos/django-ddp
project.
* Change `dddp` management command default port from 3000 to 8000.
* Validate `django.conf.settings.DATABASES` configuration on start.
* React to `django.db.models.signals.m2m_changed` model changes for
ManyToManyField.
* Add dependency on `django-dbarray`.
0.1.1 (2015-03-11)
------------------
* Add missing dependencies on `gevent`, `gevent-websocket`,
`meteor-ejson` and `psycogreen`.
* Meteor compatible latency compensation using Alea PRNG.
* Add `dddp.THREAD_LOCAL` with factories.
* Register django signals handlers via `AppConfig.ready()` handler.
* Add `dddp` management command.
* Add `dddp.models.AleaIdField` and `dddp.models.ObjectMapping` model.
* Major internal refactoring.
0.1.0 (2015-02-13)
------------------
* Working proof-of-concept.

View file

@ -2,11 +2,15 @@ include LICENSE
include *.rst
include *.sh
include *.txt
include requirements*.txt
include .gitignore
include .coveragerc
include Makefile
exclude tox.ini
graft dddp/test/meteor_todos
prune dddp/test/build
prune dddp/test/meteor_todos/.meteor/local
graft docs
prune docs/_build
include tox.ini
graft dddp/test/meteor_todos
prune dddp/test/meteor_todos/.meteor
prune docs/node_modules
exclude .travis.yml.sh
exclude .travis.yml

View file

@ -2,49 +2,53 @@ NAME := $(shell python setup.py --name)
VERSION := $(shell python setup.py --version)
SDIST := dist/${NAME}-${VERSION}.tar.gz
WHEEL_PY2 := dist/$(subst -,_,${NAME})-${VERSION}-py2-none-any.whl
WHEEL_PY3 := dist/$(subst -,_,${NAME})-${VERSION}-py3-none-any.whl
WHEEL_PYPY := dist/$(subst -,_,${NAME})-${VERSION}-pypy-none-any.whl
WHEEL := dist/$(subst -,_,${NAME})-${VERSION}-py2.py3-none-any.whl
.PHONY: all test clean clean-docs clean-dist upload-docs upload-pypi dist
.INTERMEDIATE: dist.intermediate docs
all: docs dist
all: .travis.yml docs dist
test:
tox -vvv
clean: clean-docs clean-dist
clean: clean-docs clean-dist clean-pyc
clean-docs:
$(MAKE) -C docs/ clean
clean-dist:
rm -f "${SDIST}" "${WHEEL_PY2}" "${WHEEL_PY3}"
rm -rf "${SDIST}" "${WHEEL}" dddp/test/build/ dddp/test/meteor_todos/.meteor/local/
clean-pyc:
find . -type f -name \*.pyc -print0 | xargs -0 rm -f
docs: $(shell find docs/ -type f -name \*.rst) docs/conf.py docs/Makefile $(shell find docs/_static/ -type f) $(shell find docs/_templates/ -type f) README.rst CHANGES.rst
$(MAKE) -C docs/ clean html
touch "$@"
dist: ${SDIST} ${WHEEL_PY2} ${WHEEL_PY3}
dist: ${SDIST} ${WHEEL}
@echo 'Build successful, `${MAKE} upload` when ready to release.'
${SDIST}: dist.intermediate
@echo "Testing ${SDIST}..."
tox --notest --installpkg ${SDIST}
${WHEEL_PY2}: dist.intermediate
${WHEEL_PY3}: dist.intermediate
${WHEEL_PYPY}:
tox -e pypy-test-dist
${WHEEL}: dist.intermediate
@echo "Testing ${WHEEL}..."
tox --notest --installpkg ${WHEEL}
dist.intermediate: $(shell find dddp -type f)
tox -e py27-test-dist,py34-test-dist
tox -e dist
upload: upload-pypi upload-docs
upload-pypi: ${SDIST} ${WHEEL_PY2} ${WHEEL_PY3}
twine upload "${WHEEL_PY2}" "${WHEEL_PY3}" "${SDIST}"
upload-pypi: ${SDIST} ${WHEEL}
twine upload "${WHEEL}" "${SDIST}"
upload-docs: docs/_build/
python setup.py upload_sphinx --upload-dir="$<html"
.travis.yml: tox.ini .travis.yml.sh
sh .travis.yml.sh > "$@"

View file

@ -1,3 +1,4 @@
==========
Django DDP
==========
@ -259,6 +260,7 @@ Contributors
This project is forever grateful for the love, support and respect given
by the awesome team at `Common Code`_.
.. _Django DDP: https://github.com/django-ddp/django-ddp
.. _Django: https://www.djangoproject.com/
.. _Django signals: https://docs.djangoproject.com/en/stable/topics/signals/
.. _Common Code: https://commoncode.com.au/

View file

@ -1,11 +1,11 @@
"""Django/PostgreSQL implementation of the Meteor DDP service."""
"""Django/PostgreSQL implementation of the Meteor server."""
from __future__ import unicode_literals
import os.path
import sys
from gevent.local import local
from dddp import alea
__version__ = '0.18.1'
__version__ = '0.19.0'
__url__ = 'https://github.com/django-ddp/django-ddp'
default_app_config = 'dddp.apps.DjangoDDPConfig'
@ -121,6 +121,9 @@ THREAD_LOCAL_FACTORIES = {
'alea_random': alea.Alea,
'random_streams': RandomStreams,
'serializer': serializer_factory,
'user_id': lambda: None,
'user_ddp_id': lambda: None,
'user': lambda: None,
}
THREAD_LOCAL = ThreadLocal()
METEOR_ID_CHARS = u'23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'

View file

@ -10,9 +10,8 @@ import uuid
# requirements
import dbarray
from django.conf import settings
from django.contrib.auth import get_user_model
import django.contrib.postgres.fields
from django.db import connections, router
from django.db import connections, router, transaction
from django.db.models import aggregates, Q
try:
# pylint: disable=E0611
@ -108,6 +107,7 @@ def api_endpoint(path_or_func=None, decorate=True):
Examples:
>>> from dddp.api import APIMixin, api_endpoint
>>> class Counter(APIMixin):
... value = 0
...
@ -270,6 +270,13 @@ class Collection(APIMixin):
queryset = property(get_queryset)
@property
def user_model(self):
"""Cached property getter around `get_user_model`."""
from django.contrib.auth import get_user_model
val = self.__dict__['user_model'] = get_user_model()
return val
def objects_for_user(self, user, qs=None, xmin__lte=None):
"""Find objects in queryset related to specified user."""
qs = self.get_queryset(qs)
@ -329,7 +336,7 @@ class Collection(APIMixin):
if self.always_allow_superusers:
user_ids.update(
get_user_model().objects.filter(
self.user_model.objects.filter(
is_superuser=True, is_active=True,
).values_list('pk', flat=True)
)
@ -531,15 +538,40 @@ class Publication(APIMixin):
name = None
queries = None
def get_queries(self, *params):
"""DDP get_queries - must override if using params."""
if params:
raise NotImplementedError(
'Publication params not implemented on %r publication.' % (
self.name,
),
)
return self.queries[:]
def user_queries(self, user, *params):
"""Return queries for this publication as seen by `user`."""
try:
get_queries = self.get_queries
except AttributeError:
# statically defined queries
if self.queries is None:
raise NotImplementedError(
'Must set either queries or implement get_queries method.',
)
if params:
raise NotImplementedError(
'Publication params not implemented on %r publication.' % (
self.name,
),
)
return self.queries[:]
if user is False:
# no need to play with `this.user_id` or `this.user_ddp_id`.
return get_queries(*params)
# stash the old user details
old_user_id = this.user_id
old_user_ddp_id = this.user_ddp_id
# apply the desired user details
this.user_id = None if user is None else user.pk
this.user_ddp_id = None if user is None else get_meteor_id(user)
try:
return get_queries(*params)
finally:
# restore the old user details
this.user_id = old_user_id
this.user_ddp_id = old_user_ddp_id
@api_endpoint
def collections(self, *params):
@ -548,7 +580,7 @@ class Publication(APIMixin):
set(
hasattr(qs, 'model') and model_name(qs.model) or qs[1]
for qs
in self.get_queries(*params)
in self.get_queries(False, *params)
)
)
@ -604,7 +636,7 @@ class DDP(APIMixin):
(col, qs) for (qs, col) in (
self.qs_and_collection(qs)
for qs
in pub.get_queries(*params)
in pub.user_queries(obj.user, *params)
)
)
# mergebox via MVCC! For details on how this is possible, read this:
@ -628,7 +660,7 @@ class DDP(APIMixin):
pk=obj.pk,
).order_by('pk').distinct():
other_pub = self.get_pub_by_name(other.publication)
for qs in other_pub.get_queries(*other.params):
for qs in other_pub.user_queries(other.user, *other.params):
qs, col = self.qs_and_collection(qs)
if col not in to_send:
continue
@ -645,8 +677,21 @@ class DDP(APIMixin):
@api_endpoint
def sub(self, id_, name, *params):
"""Create subscription, send matched objects that haven't been sent."""
return self.do_sub(id_, name, False, *params)
try:
return self.do_sub(id_, name, False, *params)
except Exception as err:
this.send({
'msg': 'nosub',
'id': id_,
'error': {
'error': 500,
'errorType': 'Meteor.Error',
'message': '%s' % err,
'reason': 'Subscription failed',
},
})
@transaction.atomic
def do_sub(self, id_, name, silent, *params):
"""Subscribe the current thread to the specified publication."""
try:
@ -655,6 +700,7 @@ class DDP(APIMixin):
if not silent:
this.send({
'msg': 'nosub',
'id': id_,
'error': {
'error': 404,
'errorType': 'Meteor.Error',
@ -915,10 +961,14 @@ class DDP(APIMixin):
collections__model_name=model_name(model),
).prefetch_related('collections'):
pub = self.get_pub_by_name(sub.publication)
try:
queries = list(pub.user_queries(sub.user, *sub.params))
except Exception:
queries = []
for qs, col in (
self.qs_and_collection(qs)
for qs
in pub.get_queries(*sub.params)
in queries
):
# check if obj is an instance of the model for the queryset
if qs.model is not model:

View file

@ -103,9 +103,7 @@ class DDPLauncher(object):
# shutdown existing connections
close_old_connections()
DDPLauncher.pgworker = PostgresGreenlet(
connection, debug=debug,
)
DDPLauncher.pgworker = PostgresGreenlet(connection)
# use settings.WSGI_APPLICATION or fallback to default Django WSGI app
from django.conf import settings
@ -189,6 +187,9 @@ class DDPLauncher(object):
for server in self.servers + [DDPLauncher.pgworker]:
self.logger.debug('Stopping %s', server)
server.stop()
# wait for all threads to stop.
gevent.joinall(self.threads + [DDPLauncher.pgworker])
self.threads = []
def start(self):
"""Run PostgresGreenlet and web/debug servers."""
@ -204,7 +205,12 @@ class DDPLauncher(object):
self.print('=> Started PostgresGreenlet.')
for server in self.servers:
thread = gevent.spawn(server.serve_forever)
gevent.sleep() # yield to thread in case it can't start
self.threads.append(thread)
if thread.dead:
# thread died, stop everything and re-raise the exception.
self.stop()
thread.get()
if isinstance(server, geventwebsocket.WebSocketServer):
self.print(
'=> App running at: %s://%s:%d/' % (
@ -229,6 +235,7 @@ class DDPLauncher(object):
self._stop_event.wait()
# wait for all threads to stop.
gevent.joinall(self.threads + [DDPLauncher.pgworker])
self.threads = []
def addr(val, default_port=8000, defualt_host='localhost'):

View file

@ -4,7 +4,7 @@ from __future__ import absolute_import
import collections
import os
from django.db import models, transaction
from django.db import models
from django.db.models.fields import NOT_PROVIDED
from django.conf import settings
from django.contrib.contenttypes.fields import (

View file

@ -6,15 +6,17 @@ import ejson
import gevent
import gevent.queue
import gevent.select
import os
import psycopg2 # green
import psycopg2.extensions
import socket
class PostgresGreenlet(gevent.Greenlet):
"""Greenlet for multiplexing database operations."""
def __init__(self, conn, debug=False):
def __init__(self, conn):
"""Prepare async connection."""
super(PostgresGreenlet, self).__init__()
import logging
@ -33,15 +35,41 @@ class PostgresGreenlet(gevent.Greenlet):
def _run(self): # pylint: disable=method-hidden
"""Spawn sub tasks, wait for stop signal."""
conn_params = self.connection.get_connection_params()
# See http://initd.org/psycopg/docs/module.html#psycopg2.connect and
# http://www.postgresql.org/docs/current/static/libpq-connect.html
# section 31.1.2 (Parameter Key Words) for details on available params.
conn_params.update(
async=True,
application_name='{} pid={} django-ddp'.format(
socket.gethostname(), # hostname
os.getpid(), # PID
)[:64], # 64 characters for default PostgreSQL build config
)
conn = psycopg2.connect(**conn_params)
conn = None
while conn is None:
try:
conn = psycopg2.connect(**conn_params)
except psycopg2.OperationalError as err:
# Some variants of the psycopg2 driver for Django add extra
# params that aren't meant to be passed directly to
# `psycopg2.connect()` -- issue a warning and try again.
msg = ('%s' % err).strip()
msg_prefix = 'invalid connection option "'
if not msg.startswith(msg_prefix):
# *waves hand* this is not the errror you are looking for.
raise
key = msg[len(msg_prefix):-1]
self.logger.warning(
'Ignoring unknown settings.DATABASES[%r] option: %s=%r',
self.connection.alias,
key, conn_params.pop(key),
)
self.poll(conn) # wait for conneciton to start
cur = conn.cursor()
import logging
logging.getLogger('dddp').info('=> Started PostgresGreenlet.')
cur = conn.cursor()
cur.execute('LISTEN "ddp";')
while not self._stop_event.is_set():
try:
@ -55,7 +83,9 @@ class PostgresGreenlet(gevent.Greenlet):
finally:
self.select_greenlet = None
self.poll(conn)
self.poll(conn)
cur.close()
self.poll(conn)
conn.close()
def stop(self):

View file

@ -0,0 +1,17 @@
# This file mainly exists to allow `python setup.py test` to work.
import os
import sys
import dddp
import django
from django.test.utils import get_runner
from django.conf import settings
def run_tests():
os.environ['DJANGO_SETTINGS_MODULE'] = 'dddp.test.test_project.settings'
dddp.greenify()
django.setup()
test_runner = get_runner(settings)()
failures = test_runner.run_tests(['dddp', 'dddp.test.django_todos'])
sys.exit(bool(failures))

View file

@ -1,3 +1,34 @@
from django.test import TestCase
"""Django Todos test suite."""
# Create your tests here.
import doctest
import os
import unittest
os.environ['DJANGO_SETTINGS_MODULE'] = 'dddp.test.test_project.settings'
DOCTEST_MODULES = [
]
class NoOpTest(unittest.TestCase):
def test_noop(self):
assert True
def load_tests(loader, tests, pattern):
"""Specify which test cases to run."""
del pattern
suite = unittest.TestSuite()
# add all TestCase classes from this (current) module
for attr in globals().values():
try:
if not issubclass(attr, unittest.TestCase):
continue # not subclass of TestCase
except TypeError:
continue # not a class
tests = loader.loadTestsFromTestCase(attr)
suite.addTests(tests)
# add doctests defined in DOCTEST_MODULES
for doctest_module in DOCTEST_MODULES:
suite.addTest(doctest.DocTestSuite(doctest_module))
return suite

View file

@ -62,7 +62,11 @@ WSGI_APPLICATION = 'dddp.test.test_project.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'django_ddp_test_project',
'NAME': os.environ.get('PGDATABASE', 'django_ddp_test_project'),
'USER': os.environ.get('PGUSER', os.environ['LOGNAME']),
'PORT': int(os.environ.get('PGPORT', '0')) or None,
'PASSWORD': os.environ.get('PGPASSWORD', '') or None,
'HOST': os.environ.get('PGHOST', '') or None,
}
}

99
dddp/tests.py Normal file
View file

@ -0,0 +1,99 @@
"""Django DDP test suite."""
from __future__ import unicode_literals
import doctest
import errno
import os
import socket
import unittest
from django.test import TestCase
import dddp.alea
from dddp.main import DDPLauncher
# pylint: disable=E0611, F0401
from six.moves.urllib_parse import urljoin
os.environ['DJANGO_SETTINGS_MODULE'] = 'dddp.test.test_project.settings'
DOCTEST_MODULES = [
dddp.alea,
]
class DDPServerTestCase(TestCase):
"""Test case that starts a DDP server."""
server_addr = '127.0.0.1'
server_port_range = range(8000, 8080)
ssl_certfile_path = None
ssl_keyfile_path = None
def setUp(self):
"""Fire up the DDP server."""
self.server_port = 8000
kwargs = {}
if self.ssl_certfile_path:
kwargs['certfile'] = self.ssl_certfile_path
if self.ssl_keyfile_path:
kwargs['keyfile'] = self.ssl_keyfile_path
self.scheme = 'https' if kwargs else 'http'
for server_port in self.server_port_range:
self.server = DDPLauncher(debug=True)
self.server.add_web_servers(
[
(self.server_addr, server_port),
],
**kwargs
)
try:
self.server.start()
self.server_port = server_port
return # server started
except socket.error as err:
if err.errno != errno.EADDRINUSE:
raise # error wasn't "address in use", re-raise.
continue # port in use, try next port.
raise RuntimeError('Failed to start DDP server.')
def tearDown(self):
"""Shut down the DDP server."""
self.server.stop()
def url(self, path):
"""Return full URL for given path."""
return urljoin(
'%s://%s:%d' % (self.scheme, self.server_addr, self.server_port),
path,
)
class LaunchTestCase(DDPServerTestCase):
"""Test that server launches and handles GET request."""
def test_get(self):
"""Perform HTTP GET."""
import requests
resp = requests.get(self.url('/'))
self.assertEqual(resp.status_code, 200)
def load_tests(loader, tests, pattern):
"""Specify which test cases to run."""
del pattern
suite = unittest.TestSuite()
# add all TestCase classes from this (current) module
for attr in globals().values():
if attr is DDPServerTestCase:
continue # not meant to be executed, is has no tests.
try:
if not issubclass(attr, unittest.TestCase):
continue # not subclass of TestCase
except TypeError:
continue # not a class
tests = loader.loadTestsFromTestCase(attr)
suite.addTests(tests)
# add doctests defined in DOCTEST_MODULES
for doctest_module in DOCTEST_MODULES:
suite.addTest(doctest.DocTestSuite(doctest_module))
return suite

View file

@ -3,7 +3,6 @@ from __future__ import absolute_import, unicode_literals
from copy import deepcopy
import io
import logging
import mimetypes
import os.path
@ -16,18 +15,22 @@ import pybars
# from https://www.xormedia.com/recursively-merge-dictionaries-in-python/
def dict_merge(a, b):
'''recursively merges dict's. not just simple a['key'] = b['key'], if
both a and bhave a key who's value is a dict then dict_merge is called
on both values and the result stored in the returned dictionary.'''
if not isinstance(b, dict):
return b
result = deepcopy(a)
for k, v in b.iteritems():
if k in result and isinstance(result[k], dict):
result[k] = dict_merge(result[k], v)
def dict_merge(lft, rgt):
"""
Recursive dict merge.
Recursively merges dict's. not just simple lft['key'] = rgt['key'], if
both lft and rgt have a key who's value is a dict then dict_merge is
called on both values and the result stored in the returned dictionary.
"""
if not isinstance(rgt, dict):
return rgt
result = deepcopy(lft)
for key, val in rgt.iteritems():
if key in result and isinstance(result[key], dict):
result[key] = dict_merge(result[key], val)
else:
result[k] = deepcopy(v)
result[key] = deepcopy(val)
return result
@ -54,16 +57,11 @@ class MeteorView(View):
"""Django DDP Meteor server view."""
logger = logging.getLogger(__name__)
http_method_names = ['get', 'head']
json_path = None
runtime_config = None
manifest = None
program_json = None
program_json_path = None
star_json = None # top level layout
url_map = None
@ -82,7 +80,7 @@ class MeteorView(View):
"""
Initialisation for Django DDP server view.
`Meteor.settings` is sourced from the following (later take precedence):
The following items populate `Meteor.settings` (later take precedence):
1. django.conf.settings.METEOR_SETTINGS
2. os.environ['METEOR_SETTINGS']
3. MeteorView.meteor_settings (class attribute) or empty dict
@ -96,16 +94,14 @@ class MeteorView(View):
4. MeteorView.as_view(meteor_public_envs=...)
"""
self.runtime_config = {}
self.meteor_settings = reduce(
dict_merge,
[
self.meteor_settings = {}
for other in [
getattr(settings, 'METEOR_SETTINGS', {}),
loads(os.environ.get('METEOR_SETTINGS', '{}')),
self.meteor_settings or {},
kwargs.pop('meteor_settings', {}),
],
{},
)
]:
self.meteor_settings = dict_merge(self.meteor_settings, other)
self.meteor_public_envs = set()
self.meteor_public_envs.update(
getattr(settings, 'METEOR_PUBLIC_ENVS', []),
@ -251,18 +247,14 @@ class MeteorView(View):
def get(self, request, path):
"""Return HTML (or other related content) for Meteor."""
self.logger.debug(
'[%s:%s] %s %s %s',
request.META['REMOTE_ADDR'], request.META['REMOTE_PORT'],
request.method, request.path,
request.META['SERVER_PROTOCOL'],
)
if path == 'meteor_runtime_config.js':
config = {
'DDP_DEFAULT_CONNECTION_URL': request.build_absolute_uri('/'),
'PUBLIC_SETTINGS': self.meteor_settings.get('public', {}),
'ROOT_URL': request.build_absolute_uri(
'%s/' % self.runtime_config.get('ROOT_URL_PATH_PREFIX', ''),
'%s/' % (
self.runtime_config.get('ROOT_URL_PATH_PREFIX', ''),
),
),
'ROOT_URL_PATH_PREFIX': '',
}

View file

@ -12,7 +12,9 @@ import traceback
from six.moves import range as irange
import ejson
import gevent
import geventwebsocket
from django.core import signals
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest
from django.db import connection, transaction
@ -151,6 +153,7 @@ class DDPWebSocketApplication(geventwebsocket.WebSocketApplication):
del self.pgworker.connections[self.connection.pk]
self.connection.delete()
self.connection = None
signals.request_finished.send(sender=self.__class__)
self.logger.info('- %s %s', self, args or 'CLOSE')
def on_message(self, message):
@ -195,6 +198,11 @@ class DDPWebSocketApplication(geventwebsocket.WebSocketApplication):
except Exception as err:
traceback.print_exc()
self.error(err)
# emit request_finished signal to close DB connections
signals.request_finished.send(sender=self.__class__)
if msgs:
# yield to other greenlets before processing next msg
gevent.sleep()
except geventwebsocket.WebSocketError as err:
self.ws.close()

View file

@ -1,5 +0,0 @@
-r requirements.txt
Sphinx==1.3.1
Sphinx-PyPI-upload==0.2.1
cloud_sptheme==1.7
twine==1.6.4

View file

@ -4,7 +4,7 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
PAPER = a4
BUILDDIR = _build
# User-friendly check for sphinx-build
@ -175,3 +175,13 @@ pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
pdf:
$(SPHINXBUILD) -b pdf $(ALLSPHINXOPTS) _build/pdf
@echo
@echo "Build finished. The PDF files are in _build/pdf."
dash:
$(SPHINXBUILD) -b dash $(ALLSPHINXOPTS) _build/dash
@echo
@echo "Build finished. The DASH files are in _build/dash."

BIN
docs/_static/django-ddp-logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

43
docs/admin/index.rst Normal file
View file

@ -0,0 +1,43 @@
***********************
Administration Handbook
***********************
This section is aimed at DevOps and project administrators to assist in
installing and maintaining a site using Django DDP.
Requirements
============
You must be using PostgreSQL_ with psycopg2_ in your Django_ project for
django-ddp to work. There is no requirement on any asynchronous
framework such as Reddis or crossbar.io as they are simply not needed
given the asynchronous support provided by PostgreSQL_ with psycopg2_.
Installation
============
Install the latest release from pypi (recommended):
.. code:: sh
pip install django-ddp
Don't forget to add `dddp` to your `requirements.txt` and/or the
`install_requires` section in `setup.py` for your project as necessary.
Clone and use development version direct from GitHub to test pre-release
code (no GitHub account required):
.. code:: sh
pip install -e
git+https://github.com/commoncode/django-ddp@develop#egg=django-ddp
.. _Django: https://www.djangoproject.com/
.. _Django signals: https://docs.djangoproject.com/en/stable/topics/signals/
.. _Gevent: http://www.gevent.org/
.. _PostgreSQL: http://postgresql.org/
.. _psycopg2: http://initd.org/psycopg/
.. _WebSockets: http://www.w3.org/TR/websockets/

View file

@ -1,3 +0,0 @@
.. _changelog:
.. include:: ../CHANGES.rst

View file

@ -15,7 +15,29 @@
import sys
import os
import cloud_sptheme as csp
import django
from django.conf import settings
settings.configure(
INSTALLED_APPS=[
'django.contrib.auth',
'django.contrib.contenttypes',
'dddp',
],
DATABASES={
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': os.environ.get('PGDATABASE', 'django_ddp_docs_project'),
'USER': os.environ.get('PGUSER', os.environ['LOGNAME']),
'PORT': int(os.environ.get('PGPORT', '0')) or None,
'PASSWORD': os.environ.get('PGPASSWORD', '') or None,
'HOST': os.environ.get('PGHOST', '') or None,
},
},
)
django.setup()
import alabaster
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@ -32,13 +54,19 @@ import cloud_sptheme as csp
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.napoleon',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'cloud_sptheme.ext.index_styling',
'cloud_sptheme.ext.relbar_toc',
'rst2pdf.pdfbuilder',
'sphinxcontrib.dashbuilder',
'alabaster',
]
# Show `.. todo::` items in output.
todo_include_todos = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@ -60,9 +88,9 @@ copyright = u'2015, Tyson Clugg'
# built documents.
#
# The short X.Y version.
version = '0.18'
version = '0.19'
# The full version, including alpha/beta/rc tags.
release = '0.18.1'
release = '0.19.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -107,17 +135,22 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'cloud'
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
'max_width': '80em',
'logo': 'django-ddp-logo.png',
'github_user': 'django-ddp',
'github_repo': 'django-ddp',
'github_button': 'true',
'github_type': 'star',
'github_banner': 'true',
}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = [csp.get_theme_dir()]
html_theme_path = [alabaster.get_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
@ -154,7 +187,15 @@ html_static_path = ['_static']
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
html_sidebars = {
'**': [
'about.html',
'navigation.html',
'relations.html',
'searchbox.html',
'donate.html',
],
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
@ -267,3 +308,21 @@ texinfo_documents = [
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# Grouping the document tree into PDF files. List of tuples
# (source start file, target name, title, author, options).
#
# If there is more than one author, separate them with \\.
# For example: r'Guido van Rossum\\Fred L. Drake, Jr., editor'
#
# The options element is a dictionary that lets you override
# this config per-document.
# For example,
# ('index', u'MyProject', u'My Project', u'Author Name',
# dict(pdf_compressed = True))
# would mean that specific document would be compressed
# regardless of the global pdf_compressed setting.
pdf_documents = [
('changes', u'django-ddp', u'django-ddp', u'Tyson Clugg'),
]

View file

@ -0,0 +1,5 @@
dddp.api
--------
.. automodule:: dddp.api
:members:

8
docs/devel/api/index.rst Normal file
View file

@ -0,0 +1,8 @@
=============
API Reference
=============
.. toctree::
:glob:
*

7
docs/devel/index.rst Normal file
View file

@ -0,0 +1,7 @@
===================
Developers Handbook
===================
.. toctree::
api/index

22
docs/gulpfile.js Normal file
View file

@ -0,0 +1,22 @@
var exec = require('child_process').exec;
var gulp = require('gulp');
var browserSync = require('browser-sync');
gulp.task('sphinx', function(cb) {
exec('make html', function(err, stdout, stderr) {
console.log(stdout);
console.log(stderr);
cb(err);
browserSync.reload();
});
});
gulp.task('default', ['sphinx'], function() {
browserSync({
open: false,
server: {
baseDir: '_build/html/'
}
});
gulp.watch(["../README.rst", "../LICENSE", "../CHANGES.rst", "**/*.rst", "_static/**", "conf.py"], ['sphinx']);
});

View file

@ -1,51 +1,31 @@
.. Django DDP documentation master file, created by
sphinx-quickstart on Tue Oct 13 23:24:39 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
##########
Django DDP
##########
.. include:: ../README.rst
.. image:: _static/django-ddp-logo.png
:alt: django-ddp — reactive ★ realtime ★ relational ★ robust
:align: center
Installation:
-------------
`Django DDP`_ is available for installation direct from PyPi_:
.. code:: bash
pip install django-ddp
Links
-----
.. image:: https://readthedocs.org/projects/django-ddp/badge/?version=latest
:target: https://readthedocs.org/projects/django-ddp/?badge=latest
:alt: Documentation Status
:align: right
The latest documentation is available online at
https://django-ddp.readthedocs.org/.
Source code is available online at https://github.com/commoncode/django-ddp.
.. rubric::
Django DDP lets you create websites and mobile apps that are
reactive, realtime, relational & robust -- using Django and
Meteor together with the smallest server (and environmental)
footprint.
Contents
--------
.. toctree::
:glob:
:maxdepth: 1
:maxdepth: 2
changelog
django-ddp*
Indices and tables
==================
readme
tutorial/index
admin/index
devel/index
reference/index
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
License
=======
.. include:: ../LICENSE
.. _pypi: https://pypi.python.org/pypi/django-ddp

3
docs/readme.rst Normal file
View file

@ -0,0 +1,3 @@
.. _readme:
.. include:: ../README.rst

View file

@ -0,0 +1,3 @@
.. _changelog:
.. include:: ../../CHANGES.rst

9
docs/reference/index.rst Normal file
View file

@ -0,0 +1,9 @@
=========
Reference
=========
.. toctree::
:maxdepth: 1
changelog
license

View file

@ -0,0 +1,5 @@
License
-------
.. include:: ../../LICENSE
:literal:

12
docs/tutorial/index.rst Normal file
View file

@ -0,0 +1,12 @@
========
Tutorial
========
.. todo:: This documentation is incomplete -- pull requests are welcome!
* Install Python / virtualenv / Meteor
* Setup repo / add basics:
- setup.py
- django-admin.py startproject
- meteor create <path>
- letsencrypt

6
requirements-dev.txt Normal file
View file

@ -0,0 +1,6 @@
# things you need to build from source and distribute a release
Sphinx==1.3.3
Sphinx-PyPI-upload==0.2.1
twine==1.6.4
sphinxcontrib-dashbuilder==0.1.0
rst2pdf==0.93

2
requirements-test.txt Normal file
View file

@ -0,0 +1,2 @@
# things required to run test suite
requests==2.9.0

View file

@ -1,5 +1,7 @@
Django==1.8.5
gevent==1.0.2
Django>=1.7
gevent==1.0.2 ; platform_python_implementation == "CPython" and python_version < "3.0"
gevent==1.1rc2 ; platform_python_implementation != "CPython" or python_version >= "3.0"
gevent-websocket==0.9.5
psycopg2==2.6.1
psycopg2==2.6.1 ; platform_python_implementation == "CPython"
psycopg2cffi>=2.7.2 ; platform_python_implementation != "CPython"
six==1.10.0

View file

@ -1,2 +1,2 @@
[bdist_wheel]
universal=0
universal=1

131
setup.py
View file

@ -1,8 +1,43 @@
#!/usr/bin/env python
"""Django/PostgreSQL implementation of the Meteor DDP service."""
import platform
import sys
from setuptools import setup, find_packages
"""Django/PostgreSQL implementation of the Meteor server."""
import os.path
import setuptools
import subprocess
from distutils import log
from distutils.version import StrictVersion
from distutils.command.build import build
# setuptools 18.5 introduces support for the `platform_python_implementation`
# environment marker: https://github.com/jaraco/setuptools/pull/28
__requires__ = 'setuptools>=18.5'
assert StrictVersion(setuptools.__version__) >= StrictVersion('18.5'), \
'Installation from source requires setuptools>=18.5.'
class Build(build):
"""Build all files of a package."""
def run(self):
"""Build our package."""
cmdline = [
'meteor',
'build',
'--directory',
'../build',
]
meteor_dir = os.path.join(
os.path.dirname(__file__),
'dddp',
'test',
'meteor_todos',
)
log.info('Building meteor app %r (%s)', meteor_dir, ' '.join(cmdline))
subprocess.check_call(cmdline, cwd=meteor_dir)
return build.run(self)
CLASSIFIERS = [
# Beta status until 1.0 is released
@ -40,51 +75,85 @@ CLASSIFIERS = [
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Framework :: Django :: 1.7",
"Framework :: Django :: 1.8",
"Framework :: Django :: 1.9",
]
# Ensure correct dependencies between different python implementations.
IMPLEMENTATION_INSTALL_REQUIRES = {
# extra requirements for CPython implementation
'CPython': [
'psycopg2>=2.5.4',
'gevent>=1.1b6' if sys.version_info >= (3, 0) else 'gevent>=1.0',
],
# extra requirements for all other Python implementations
None: [
'psycopg2cffi>=2.7.2',
'gevent>=1.1b6',
],
}
setup(
setuptools.setup(
name='django-ddp',
version='0.18.1',
version='0.19.0',
description=__doc__,
long_description=open('README.rst').read(),
author='Tyson Clugg',
author_email='tyson@clugg.net',
url='https://github.com/commoncode/django-ddp',
url='https://github.com/django-ddp/django-ddp',
keywords=[
'django ddp meteor websocket websockets realtime real-time live '
'liveupdate live-update livequery live-query'
],
license='MIT',
packages=find_packages(),
include_package_data=True,
packages=setuptools.find_packages(),
include_package_data=True, # install data files specified in MANIFEST.in
zip_safe=False, # TODO: Move dddp.test into it's own package.
setup_requires=[
# packages required to run the setup script
__requires__,
],
install_requires=[
'Django>=1.7',
'gevent-websocket>=0.9,!=0.9.4',
'Django>=1.8',
'django-dbarray>=0.2',
'meteor-ejson>=1.0',
'psycogreen>=1.0',
'django-dbarray>=0.2',
'pybars3>=0.9.1',
'six>=1.10.0',
] + IMPLEMENTATION_INSTALL_REQUIRES.get(
platform.python_implementation(),
IMPLEMENTATION_INSTALL_REQUIRES[None], # default to non-CPython reqs
),
],
extras_require={
# We need gevent version dependent upon environment markers, but the
# extras_require seem to be a separate phase from setup/install of
# install_requires. So we specify gevent-websocket (which depends on
# gevent) here in order to honour environment markers.
'': [
'gevent-websocket>=0.9,!=0.9.4',
],
# Django 1.9 doesn't support Python 3.3
':python_version=="3.3"': [
'Django<1.9',
],
# CPython < 3.0 can use gevent 1.0
':platform_python_implementation=="CPython" and python_version<"3.0"': [
'gevent>=1.0',
],
# everything else needs gevent 1.1
':platform_python_implementation!="CPython" or python_version>="3.0"': [
'gevent>=1.1rc2',
],
# CPython can use plain old psycopg2
':platform_python_implementation=="CPython"': [
'psycopg2>=2.5.4',
],
# everything else must use psycopg2cffi
':platform_python_implementation != "CPython"': [
'psycopg2cffi>=2.7.2',
],
'develop': [
# things you need to distribute a wheel from source (`make dist`)
'Sphinx>=1.3.3',
'Sphinx-PyPI-upload>=0.2.1',
'twine>=1.6.4',
'sphinxcontrib-dashbuilder>=0.1.0',
],
},
entry_points={
'console_scripts': [
'dddp=dddp.main:main',
],
},
classifiers=CLASSIFIERS,
test_suite='dddp.test.run_tests',
tests_require=[
'requests',
],
cmdclass={
'build': Build,
},
)

View file

@ -1 +0,0 @@
-r requirements.txt

169
tox.ini
View file

@ -4,45 +4,158 @@
# and then run "tox" from this directory.
[tox]
# require tox>=2.1.1 or refuse to run the tests.
# require tox 2.1.1 or later
minversion=2.1.1
# return success even if some of the specified environments are missing
# don't fail if missing a python version specified in envlist
skip_missing_interpreters=True
# "envlist" is a comma separated list of environments, each environment name
# contains factors separated by hyphens. For example, "py27-unittest" has 2
# factors: "py27" and "unittest". Other settings such as "setenv" accept the
# factor names as a prefixes (eg: "unittest: ...") so that prefixed settings
# only apply if the environment being run contains that factor.
# list of environments to run by default
envlist =
py27-test,
py33-test,
py34-test,
py35-test,
pypy-test,
lint
clean
py33-django{1.8}
{py27,py34,py35,pypy,pypy3}-django{1.8,1.9}
report
[testenv]
# virtualenv only installs setuptools==0.18.2 but we need 0.18.5:
# - https://github.com/pypa/virtualenv/issues/807
# - https://github.com/pypa/virtualenv/issues/801
# - https://github.com/pypa/virtualenv/issues/717
# - https://github.com/pypa/virtualenv/issues/781
# - https://github.com/pypa/virtualenv/issues/580
# - https://github.com/pypa/virtualenv/issues/563
# - https://github.com/pypa/virtualenv/issues/491
# wheel 0.25.0 needed for Python 3.5:
# - https://bitbucket.org/pypa/wheel/issues/146/wheel-building-fails-on-cpython-350b3
install_command=sh -c 'pip install -U "setuptools>=18.5" "wheel>=0.25.0" "pip>=7.1.2" && pip install "$@" && sync' sh {opts} {packages}
whitelist_externals=sh
# force clean environment each time
recreate=True
usedevelop=True
# build sdist from setup.py and install from that (validate setup.py)
usedevelop=False
# list of environment variables passed through to commands
passenv=
BUILD_NUMBER
BUILD_URL
XDG_CACHE_HOME
; https://help.ubuntu.com/community/EnvironmentVariables#Other_environment_variables
USER
LOGNAME
HOME
TERM
TERMCAP
# stop running commands if previous commands fail
ignore_errors = False
; https://help.ubuntu.com/community/EnvironmentVariables#Graphical_desktop-related_variables
DISPLAY
XDG_CACHE_HOME
C_INCLUDE_PATH
CFLAGS
; https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project
BUILD_NUMBER
BUILD_ID
BUILD_URL
NODE_NAME
JOB_NAME
BUILD_TAG
JENKINS_URL
EXECUTOR_NUMBER
JAVA_HOME
WORKSPACE
GIT_COMMIT
GIT_URL
GIT_BRANCH
; http://www.postgresql.org/docs/current/static/libpq-envars.html
PGHOST
PGHOSTADDR
PGPORT
PGDATABASE
PGUSER
PGPASSWORD
PGPASSFILE
PGSERVICE
PGSERVICEFILE
PGREALM
PGOPTIONS
PGAPPNAME
PGSSLMODE
PGREQUIRESSL
PGSSLCOMPRESSION
PGSSLCERT
PGSSLKEY
PGSSLROOTCERT
PGSSLCRL
PGREQUIREPEER
PGKRBSRVNAME
PGSSLLIB
PGCONNECT_TIMEOUT
PGCLIENTENCODING
PGDATESTYLE
PGTZ
PGGEQO
PGSYSCONFDIR
PGLOCALEDIR
# `pip install -rrequierements.txt` <-- tox doesn't understand PEP-0496 Environment Markers.
# pypy coverage fails with --concurrency set to `gevent`
# pypy install gevent fails building wheel
commands =
dist: check-manifest
py{27,33,34,35}-test: coverage run --concurrency=gevent {toxinidir}/dddp/test/manage.py test -v3 --noinput
pypy-test: coverage run {toxinidir}/dddp/test/manage.py test -v3 --noinput
test: coverage report
dist: {envpython} setup.py --quiet --no-user-cfg sdist --dist-dir={toxinidir}/dist/
dist: {envpython} setup.py --quiet --no-user-cfg bdist_wheel --dist-dir={toxinidir}/dist/
{py27,py33,py34,py35}: pip install -rrequirements.txt
{pypy,pypy3}: pip install --no-binary gevent -rrequirements.txt
{py27,py33,py34,py35}: coverage run --append --concurrency=gevent --source=dddp setup.py test
{pypy,pypy3}: coverage run --append --source=dddp setup.py test
deps =
test: coverage
dist: check-manifest
dist: wheel
#-rrequirements.txt
django1.8: Django>=1.8,<1.9
django1.9: Django>=1.9,<1.10
coverage
[testenv:dist]
install_command=sh -c 'pip install -U "setuptools>=18.5" "wheel>=0.25.0" "pip>=7.1.2" && pip install "$@" && sync' sh {opts} {packages}
whitelist_externals=sh
commands =
check-manifest --ignore "dddp/test/build*,dddp/test/meteor_todos/.meteor/local*"
{envpython} setup.py --no-user-cfg sdist --dist-dir={toxinidir}/dist/
{envpython} setup.py --no-user-cfg bdist_wheel --dist-dir={toxinidir}/dist/
sh -c "cd docs && sphinx-build -b html -d _build/doctrees -D latex_paper_size=a4 . _build/html"
usedevelop=True
deps =
-rrequirements.txt
-rrequirements-dev.txt
check-manifest
wheel
[testenv:clean]
skip_install=True
deps=coverage
commands=
coverage erase
[testenv:report]
skip_install=True
deps=coverage
commands=
coverage report
coverage html
[testenv:lint]
usedevelop=True
commands=
pip install -rrequirements.txt
prospector --doc-warnings --zero-exit {toxinidir}/dddp/
deps =
prospector==0.10.2
pylint==1.4.5