Merge pull request #3 from MEERQAT/feature/MQ-683-port-python-3.7

[MQ-683] Port django-ddp to Python 3.7
This commit is contained in:
Adamos Kyriakou 2021-03-10 04:31:14 +02:00 committed by GitHub
commit 3c1475f09b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 110 additions and 130 deletions

1
.gitignore vendored
View file

@ -29,3 +29,4 @@ docs/node_modules/
# meteor
test/build/
test/meteor_todos/.meteor/
/.idea

View file

@ -1,10 +1,10 @@
"""Django/PostgreSQL implementation of the Meteor server."""
from __future__ import unicode_literals
import sys
from gevent.local import local
from dddp import alea
__version__ = '0.19.1'
__version__ = '0.20.0'
__url__ = 'https://github.com/django-ddp/django-ddp'
default_app_config = 'dddp.apps.DjangoDDPConfig'
@ -71,9 +71,9 @@ class MeteorError(Exception):
error, reason, details, err_kwargs = self.args
result = {
key: val
for key, val in {
for key, val in list({
'error': error, 'reason': reason, 'details': details,
}.items()
}.items())
if val is not None
}
result.update(err_kwargs)
@ -165,7 +165,7 @@ THREAD_LOCAL_FACTORIES = {
'user': lambda: None,
}
THREAD_LOCAL = this = ThreadLocal() # pylint: disable=invalid-name
METEOR_ID_CHARS = u'23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'
METEOR_ID_CHARS = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'
def meteor_random_id(name=None, length=17):

View file

@ -9,6 +9,7 @@ from binascii import Error
import collections
import datetime
import hashlib
import base64
from ejson import loads, dumps
@ -72,20 +73,19 @@ def iter_auth_hashes(user, purpose, minutes_valid):
"""
now = timezone.now().replace(microsecond=0, second=0)
for minute in range(minutes_valid + 1):
yield hashlib.sha1(
'%s:%s:%s:%s:%s' % (
now - datetime.timedelta(minutes=minute),
user.password,
purpose,
user.pk,
settings.SECRET_KEY,
),
).hexdigest()
_content = '%s:%s:%s:%s:%s' % (
now - datetime.timedelta(minutes=minute),
user.password,
purpose,
user.pk,
settings.SECRET_KEY,
)
yield hashlib.sha1(_content.encode()).hexdigest()
def get_auth_hash(user, purpose):
"""Generate a user hash for a particular purpose."""
return iter_auth_hashes(user, purpose, minutes_valid=1).next()
return next(iter_auth_hashes(user, purpose, minutes_valid=1))
def calc_expiry_time(minutes_valid):
@ -97,15 +97,19 @@ def calc_expiry_time(minutes_valid):
def get_user_token(user, purpose, minutes_valid):
"""Return login token info for given user."""
token = ''.join(
dumps([
user.get_username(),
get_auth_hash(user, purpose),
]).encode('base64').split('\n')
token_json = dumps([
user.get_username(),
get_auth_hash(user, purpose),
])
token_json_enc = token_json.encode()
token_json_enc_b64_multiline = base64.b64encode(token_json_enc)
token_json_enc_b64_singleline = "".join(
token_json_enc_b64_multiline.decode().split("\n")
)
return {
'id': get_meteor_id(user),
'token': token,
'token': token_json_enc_b64_singleline,
'tokenExpires': calc_expiry_time(minutes_valid),
}
@ -185,7 +189,7 @@ class Users(Collection):
"""Return name prefixed by `key_prefix`."""
return '%s%s' % (key_prefix, name)
for key in profile.keys():
for key in list(profile.keys()):
val = getter(key)
if key == prefixed('name'):
result['full_name'] = val
@ -208,7 +212,7 @@ class Users(Collection):
if len(update['$set']) != 0:
raise MeteorError(400, 'Invalid update fields: %r')
for key, val in profile_update.items():
for key, val in list(profile_update.items()):
setattr(user, key, val)
user.save()
@ -275,7 +279,7 @@ class Auth(APIMixin):
])
# first pass, send `added` for objs unique to `post`
for col_post, query in post.items():
for col_post, query in list(post.items()):
try:
qs_pre = pre[col_post]
query = query.exclude(
@ -288,7 +292,7 @@ class Auth(APIMixin):
this.ws.send(col_post.obj_change_as_msg(obj, ADDED))
# second pass, send `removed` for objs unique to `pre`
for col_pre, query in pre.items():
for col_pre, query in list(pre.items()):
try:
qs_post = post[col_pre]
query = query.exclude(
@ -314,7 +318,7 @@ class Auth(APIMixin):
def validated_user(cls, token, purpose, minutes_valid):
"""Resolve and validate auth token, returns user object."""
try:
username, auth_hash = loads(token.decode('base64'))
username, auth_hash = loads(base64.b64decode(token))
except (ValueError, Error):
cls.auth_failed(token=token)
try:
@ -343,10 +347,10 @@ class Auth(APIMixin):
def get_username(self, user):
"""Retrieve username from user selector."""
if isinstance(user, basestring):
if isinstance(user, str):
return user
elif isinstance(user, dict) and len(user) == 1:
[(key, val)] = user.items()
[(key, val)] = list(user.items())
if key == 'username' or (key == self.user_model.USERNAME_FIELD):
# username provided directly
return val
@ -373,7 +377,7 @@ class Auth(APIMixin):
@staticmethod
def get_password(password):
"""Return password in plain-text from string/dict."""
if isinstance(password, basestring):
if isinstance(password, str):
# regular Django authentication - plaintext password... but you're
# using HTTPS (SSL) anyway so it's protected anyway, right?
return password

View file

@ -1,5 +1,5 @@
"""Django DDP Accounts test suite."""
from __future__ import unicode_literals
import sys
from dddp import tests

View file

@ -1,4 +1,4 @@
from __future__ import unicode_literals
from django.contrib import admin
from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.html import format_html
@ -7,7 +7,7 @@ from dddp import models
def object_admin_link(obj):
kwargs = {
'format_string': u'{app_label}.{model} {object_id}: {object}',
'format_string': '{app_label}.{model} {object_id}: {object}',
'app_label': obj.content_type.app_label,
'model': obj.content_type.model,
'object_id': obj.object_id,

View file

@ -49,7 +49,7 @@ True
True
"""
from __future__ import unicode_literals
from math import floor
import os

View file

@ -1,5 +1,5 @@
"""Django DDP API, Collections, Cursors and Publications."""
from __future__ import absolute_import, unicode_literals, print_function
# standard library
import collections
@ -241,7 +241,7 @@ class Collection(APIMixin):
if user_rels:
if user is None:
return qs.none() # no user but we need one: return no objects.
if isinstance(user_rels, basestring):
if isinstance(user_rels, str):
user_rels = [user_rels]
user_filter = None
# Django supports model._meta -> pylint: disable=W0212
@ -283,7 +283,7 @@ class Collection(APIMixin):
if obj.pk is None:
return user_ids # nobody can see objects that don't exist
user_rels = self.user_rel
if isinstance(user_rels, basestring):
if isinstance(user_rels, str):
user_rels = [user_rels]
user_rel_map = {
'_user_rel_%d' % index: ArrayAgg(user_rel)
@ -303,7 +303,7 @@ class Collection(APIMixin):
).annotate(
**user_rel_map
).values_list(
*user_rel_map.keys()
*list(user_rel_map.keys())
).get():
user_ids.update(rel_user_ids)
user_ids.difference_update([None])
@ -411,15 +411,15 @@ class Collection(APIMixin):
"""Generate a DDP msg for obj with specified msg type."""
# check for F expressions
exps = [
name for name, val in vars(obj).items()
name for name, val in list(vars(obj).items())
if isinstance(val, ExpressionNode)
]
if exps:
# clone/update obj with values but only for the expression fields
obj = deepcopy(obj)
for name, val in self.model.objects.values(*exps).get(
for name, val in list(self.model.objects.values(*exps).get(
pk=obj.pk,
).items():
).items()):
setattr(obj, name, val)
# run serialization now all fields are "concrete" (not F expressions)
@ -448,9 +448,9 @@ class Collection(APIMixin):
# This will be sent as the `id`, don't send it in `fields`.
fields.pop(field.name)
for field in meta.local_many_to_many:
fields['%s_ids' % field.name] = get_meteor_ids(
fields['%s_ids' % field.name] = list(get_meteor_ids(
field.rel.to, fields.pop(field.name),
).values()
).values())
return data
def obj_change_as_msg(self, obj, msg, meteor_ids=None):
@ -572,7 +572,7 @@ class DDP(APIMixin):
@property
def api_providers(self):
"""Return an iterable of API providers."""
return self._registry.values()
return list(self._registry.values())
def qs_and_collection(self, qs):
"""Return (qs, collection) from qs (which may be a tuple)."""
@ -608,7 +608,7 @@ class DDP(APIMixin):
),
)
for col, qs
in queries.items()
in list(queries.items())
)
for other in Subscription.objects.filter(
connection=obj.connection_id,
@ -628,7 +628,7 @@ class DDP(APIMixin):
*args, **kwargs
).values('pk'),
)
for col, qs in to_send.items():
for col, qs in list(to_send.items()):
yield col, qs.distinct()
@api_endpoint

View file

@ -19,7 +19,7 @@ class DjangoDDPConfig(AppConfig):
"""Initialisation for django-ddp (setup lookups and signal handlers)."""
if not settings.DATABASES:
raise ImproperlyConfigured('No databases configured.')
for (alias, conf) in settings.DATABASES.items():
for (alias, conf) in list(settings.DATABASES.items()):
engine = conf['ENGINE']
if engine not in [
'django.db.backends.postgresql',

View file

@ -1,5 +1,5 @@
"""Django DDP logging helpers."""
from __future__ import absolute_import, print_function
import datetime
import logging

View file

@ -1,7 +1,5 @@
"""Django DDP WebSocket service."""
from __future__ import absolute_import, print_function
import argparse
import collections
import os
@ -11,6 +9,7 @@ import gevent
from gevent.backdoor import BackdoorServer
import gevent.event
import gevent.pywsgi
import gevent.signal
import geventwebsocket
import geventwebsocket.handler
@ -157,7 +156,7 @@ class DDPLauncher(object):
listen_addr,
self.resource,
debug=debug,
**{key: val for key, val in ssl_args.items() if val is not None}
**{key: val for key, val in list(ssl_args.items()) if val is not None}
)
def get_backdoor_server(self, listen_addr, **context):
@ -286,7 +285,7 @@ def serve(listen, verbosity=1, debug_port=0, **ssl_args):
sigmap = {
val: name
for name, val
in vars(signal).items()
in list(vars(signal).items())
if name.startswith('SIG')
}
@ -299,7 +298,7 @@ def serve(listen, verbosity=1, debug_port=0, **ssl_args):
)
launcher.stop()
for signum in [signal.SIGINT, signal.SIGQUIT]:
gevent.signal(signum, sighandler)
gevent.signal.signal(signum, sighandler)
launcher.run()

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import dddp.models

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import dddp.models

View file

@ -1,5 +1,5 @@
"""Django DDP models."""
from __future__ import absolute_import
import collections
import os
@ -109,7 +109,7 @@ def get_meteor_ids(model, object_ids):
).values_list('object_id', 'meteor_id')
for obj_pk, meteor_id in query:
result[str(obj_pk)] = meteor_id
for obj_pk, meteor_id in result.items():
for obj_pk, meteor_id in list(result.items()):
if meteor_id is None:
result[obj_pk] = get_meteor_id(model, obj_pk)
return result
@ -309,7 +309,7 @@ class Connection(models.Model, object):
def __str__(self):
"""Text representation of subscription."""
return u'%s/\u200b%s' % (
return '%s/\u200b%s' % (
self.connection_id,
self.remote_addr,
)
@ -337,7 +337,7 @@ class Subscription(models.Model, object):
def __str__(self):
"""Text representation of subscription."""
return u'%s/\u200b%s/\u200b%s: %s%s' % (
return '%s/\u200b%s/\u200b%s: %s%s' % (
self.user,
self.connection_id,
self.sub_id,
@ -367,7 +367,7 @@ class SubscriptionCollection(models.Model):
def __str__(self):
"""Human readable representation of colleciton for a subscription."""
return u'%s \u200b %s (%s)' % (
return '%s \u200b %s (%s)' % (
self.subscription,
self.collection_name,
self.model_name,

View file

@ -1,6 +1,6 @@
"""Django DDP PostgreSQL Greenlet."""
from __future__ import absolute_import
import ejson
import gevent
@ -39,7 +39,7 @@ class PostgresGreenlet(gevent.Greenlet):
# 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,
async_=True,
application_name='{} pid={} django-ddp'.format(
socket.gethostname(), # hostname
os.getpid(), # PID

View file

@ -1,5 +1,5 @@
"""Django DDP test suite."""
from __future__ import absolute_import, unicode_literals
import doctest
import errno
@ -136,7 +136,7 @@ class DDPTestServer(object):
"""DDP server with auto start and stop."""
server_addr = '127.0.0.1'
server_port_range = range(8000, 8080)
server_port_range = list(range(8000, 8080))
ssl_certfile_path = None
ssl_keyfile_path = None
@ -404,7 +404,7 @@ def load_tests(loader, tests, pattern):
del pattern
suite = unittest.TestSuite()
# add all TestCase classes from this (current) module
for attr in globals().values():
for attr in list(globals().values()):
if attr is DDPServerTestCase:
continue # not meant to be executed, is has no tests.
try:

View file

@ -1,5 +1,5 @@
"""Django DDP Server views."""
from __future__ import absolute_import, unicode_literals
from copy import deepcopy
import io
@ -26,7 +26,7 @@ def dict_merge(lft, rgt):
if not isinstance(rgt, dict):
return rgt
result = deepcopy(lft)
for key, val in rgt.iteritems():
for key, val in rgt.items():
if key in result and isinstance(result[key], dict):
result[key] = dict_merge(result[key], val)
else:
@ -233,11 +233,11 @@ class MeteorView(View):
'inline': None,
'head': read(
self.internal_map.get('head', {}).get('path_full', None),
default=u'',
default='',
),
'body': read(
self.internal_map.get('body', {}).get('path_full', None),
default=u'',
default='',
),
}
tmpl_raw = read(self.template_path, encoding='utf8')
@ -271,7 +271,7 @@ class MeteorView(View):
)
try:
file_path, content_type = self.url_map[path]
with open(file_path, 'r') as content:
with open(file_path, 'rb') as content:
return HttpResponse(
content.read(),
content_type=content_type,

View file

@ -1,6 +1,6 @@
"""Django DDP WebSocket service."""
from __future__ import absolute_import, print_function
import atexit
import collections
@ -153,8 +153,8 @@ class DDPWebSocketApplication(geventwebsocket.WebSocketApplication):
# `_tx_buffer` collects outgoing messages which must be sent in order
self._tx_buffer = {}
# track the head of the queue (buffer) and the next msg to be sent
self._tx_buffer_id_gen = itertools.cycle(irange(sys.maxint))
self._tx_next_id_gen = itertools.cycle(irange(sys.maxint))
self._tx_buffer_id_gen = itertools.cycle(irange(sys.maxsize))
self._tx_next_id_gen = itertools.cycle(irange(sys.maxsize))
# start by waiting for the very first message
self._tx_next_id = next(self._tx_next_id_gen)
@ -332,7 +332,7 @@ class DDPWebSocketApplication(geventwebsocket.WebSocketApplication):
safe_call(self.logger.debug, 'TX found %d', self._tx_next_id)
# advance next message ID
self._tx_next_id = next(self._tx_next_id_gen)
if not isinstance(data, basestring):
if not isinstance(data, str):
# ejson payload
msg = data.get('msg', None)
if msg in (ADDED, CHANGED, REMOVED):

View file

@ -3,4 +3,4 @@ Sphinx==1.3.3
Sphinx-PyPI-upload==0.2.1
twine==1.6.4
sphinxcontrib-dashbuilder==0.1.0
rst2pdf==0.93
rst2pdf==0.98

View file

@ -1,7 +1,8 @@
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 ; platform_python_implementation == "CPython"
psycopg2cffi>=2.7.2 ; platform_python_implementation != "CPython"
six==1.10.0
Django==1.11.29
meteor-ejson==1.1.0
psycogreen==1.0.2
pybars3==0.9.7
six==1.15.0
gevent==21.1.2
psycopg2==2.8.6
git+https://github.com/MEERQAT/gevent-websocket.git@v0.11.0#egg=gevent-websocket

View file

@ -14,12 +14,10 @@ import setuptools.command.build_ext
# pypi
import setuptools
# setuptools 18.5 introduces support for the `platform_python_implementation`
# environment marker: https://github.com/jaraco/setuptools/pull/28
__requires__ = 'setuptools>=18.5'
__requires__ = 'setuptools>=54.0.0'
assert StrictVersion(setuptools.__version__) >= StrictVersion('18.5'), \
'Installation from source requires setuptools>=18.5.'
assert StrictVersion(setuptools.__version__) >= StrictVersion('54.0.0'), \
'Installation from source requires setuptools>=54.0.0'
SETUP_DIR = os.path.dirname(__file__)
@ -178,7 +176,7 @@ CLASSIFIERS = [
setuptools.setup(
name='django-ddp',
version='0.19.1',
version='0.20.0',
description=__doc__,
long_description=open('README.rst').read(),
author='Tyson Clugg',
@ -197,46 +195,23 @@ setuptools.setup(
__requires__,
],
install_requires=[
'Django>=1.8',
'meteor-ejson>=1.0',
'psycogreen>=1.0',
'pybars3>=0.9.1',
'six>=1.10.0',
'Django==1.11.29',
'meteor-ejson==1.1.0',
'psycogreen==1.0.2',
'pybars3==0.9.7',
'six==1.15.0',
'gevent==21.1.2',
'psycopg2==2.8.6',
'gevent-websocket @ git+https://github.com/MEERQAT/gevent-websocket.git@v0.11.0#egg=gevent-websocket',
],
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',
'Sphinx==1.3.3',
'Sphinx-PyPI-upload==0.2.1',
'twine==1.6.4',
'sphinxcontrib-dashbuilder==0.1.0',
'rst2pdf==0.98',
],
},
entry_points={