Migration guide

From 1.1 to 2.0

This section will guide you to update an existing setup of django-modern-rpc 1.0 or 1.1 to the latest v2 release.

Update settings

MODERNRPC_METHODS_MODULES

In v2, the new Server / Namespace system will automatically import your decorated proecdures. This setting is now useless, you can simply delete it from your settings.

MODERNRPC_LOG_EXCEPTIONS

TBD

MODERNRPC_DOC_FORMAT

TBD

MODERNRPC_DEFAULT_ENTRYPOINT_NAME

The RPCEntryPoint class is not used anymore. You can define multiple RpcServer instances to split your APIs. No specific name is required, and no default one is needed anymore. This setting is now useless, you can simply delete it from your settings.

MODERNRPC_JSON_DECODER / MODERNRPC_JSON_ENCODER

Backends are now configured individually. Configuring the encoder and decoder with default builtin json backend is still possible using new settings.

Before
myproject/settings.py
 MODERNRPC_JSON_DECODER = "path.to.valid.json.Decoder"
 MODERNRPC_JSON_ENCODER = "path.to.valid.json.Encoder"
After
myproject/settings.py
 MODERNRPC_JSON_DESERIALIZER = {
     "class": "modernrpc.jsonrpc.backends.json.PythonJsonDeserializer",
     "kwargs": {
         "load_kwargs": {"cls": path.to.valid.json.Decoder},
     }
 }
 MODERNRPC_JSON_SERIALIZER = {
     "class": "modernrpc.jsonrpc.backends.json.PythonJsonSerializer",
     "kwargs": {
         "dump_kwargs": {"cls": path.to.valid.json.Encoder},
     }
 }

MODERNRPC_XMLRPC_USE_BUILTIN_TYPES / MODERNRPC_XMLRPC_ALLOW_NONE

Backends are now configured individually. Configuring the behavior of builtin xmlrpc backend is still possible using new settings.

Before
myproject/settings.py
 MODERNRPC_XMLRPC_USE_BUILTIN_TYPES = False
 MODERNRPC_XMLRPC_ALLOW_NONE = False
After
myproject/settings.py
 MODERNRPC_XML_DESERIALIZER = {
     "class": "modernrpc.xmlrpc.backends.xmlrpc.PythonXmlRpcDeserializer",
     "kwargs": {
         "load_kwargs": {"use_builtin_types": False}
     }
 }
 MODERNRPC_XML_SERIALIZER = {
     "class": "modernrpc.xmlrpc.backends.xmlrpc.PythonXmlRpcSerializer",
     "kwargs": {
         "dump_kwargs": {"allow_none": False}
     }
 }

MODERNRPC_XMLRPC_DEFAULT_ENCODING

In the previous versions, this setting was used to initialize an xmlrpc.client.Marshaller. In the v2, this class is not directly instanciated but through the serialization process. Encoding can still be configured

Before
myproject/settings.py
 MODERNRPC_XMLRPC_DEFAULT_ENCODING = "ascii"
After
myproject/settings.py
 MODERNRPC_XML_SERIALIZER = {
     "class": "modernrpc.xmlrpc.backends.xmlrpc.PythonXmlRpcSerializer",
     "kwargs": {
         "dump_kwargs": {"encoding": "ascii"}
     }
 }

Replace a single RPCEntryPoint

Before

Procedure registration was possible from anywhere in the code, as soon as the module was declared in settings.MODERNRPC_METHODS_MODULES.

myapp/remote_procedures.py
from modernrpc.core import rpc_method

@rpc_method
def add(a, b):
    return a + b
myproject/urls.py
from django.urls import path
from modernrpc.views import RPCEntryPoint

urlpatterns = [
    # ... other url patterns
    path('rpc/', RPCEntryPoint.as_view()),
]

After

With v2, an RpcServer instance must be created, and then used to register procedures0

myproject/myapp/rpc.py
from modernrpc.server import RpcServer

server = RpcServer()


@server.register_procedure
def add(a: int, b: int) -> int:
    """Add two numbers and return the result.

    :param a: First number
    :param b: Second number
    :return: Sum of a and b
    """
    return a + b
myproject/urls.py
from django.urls import path
from myapp.rpc import server

urlpatterns = [
    # ... other url patterns
    path('rpc/', server.view),
]

Note: if you declared multiple RPCEntryPoint in your urls config, simply declare multiple RpcServer instances instead, then register each procedure directly to the right server. If a procedure must be registered in 2 different servers, simply use the registration decorator multiple times.

myproject/myapp/rpc.py
from modernrpc.server import RpcServer

api_v1 = RpcServer()
api_v2 = RpcServer()


@api_v1.register_procedure()
@api_v2.register_procedure()
def add(a: int, b: int) -> int:
    """Add two numbers and return the result.

    :param a: First number
    :param b: Second number
    :return: Sum of a and b
    """
    return a + b

This new process allow to easily customize registration per procedure and per server.

Update your authentication configuration

Before

myproject/myapp/auth.py
# Custom predicate used to block some procedures to known bots
def forbid_bots_access(request):
    """Return True when request has a User-Agent different from provided list"""
    if "User-Agent" not in request.headers:
        # No User-Agent provided, the request must be rejected
        return False

    forbidden_bots = [
        'Googlebot',  # Google
        'Bingbot',  # Microsoft
        'Slurp',  # Yahoo
        'DuckDuckBot',  # DuckDuckGo
        'Baiduspider',  # Baidu
        'YandexBot',  # Yandex
        'facebot',  # Facebook
    ]

    if request.headers["User-Agent"].lower() in [ua.lower() for ua in forbidden_bots]
        # ... forbid access
        return False

    # In all other cases, allow access
    return True
from modernrpc.core import rpc_method
from modernrpc.auth import set_authentication_predicate
from modernrpc.auth.basic import http_basic_auth_permissions_required
from myproject.myapp.auth import forbid_bots_access


@rpc_method
@http_basic_auth_permissions_required(permissions='auth.view_user')
def my_rpc_method_with_builtin_predicate(username):
    user = User.objects.get(username=username).
    return f"{u.first_name} {u.last_name}"

@rpc_method
@set_authentication_predicate(forbid_bots_access)
def my_rpc_method_with_custom_authentication(a, b):
    return a + b

After

myproject/myapp/auth.py
from django.contrib.auth import authenticate

# Predicate used to block some procedures to known bots
def forbid_bots_access(request: HttpRequest):
    """Return True when request has a User-Agent different from provided list"""
    if "User-Agent" not in request.headers:
        # No User-Agent provided, the request must be rejected
        return False

    forbidden_bots = [
        'Googlebot',  # Google
        'Bingbot',  # Microsoft
        'Slurp',  # Yahoo
        'DuckDuckBot',  # DuckDuckGo
        'Baiduspider',  # Baidu
        'YandexBot',  # Yandex
        'facebot',  # Facebook
    ]

    if request.headers["User-Agent"].lower() in [ua.lower() for ua in forbidden_bots]
        # ... forbid access
        return False

    # In all other cases, allow access
    return True


# Predicate to check for specific Django permissions
def check_view_permissions(perms: str):
    def inner(request: HttpRequest):
        # Use modernrpc helper to extract Basic Auth username & password
        username, password = extract_http_basic_auth(request)
        # Use Django auth system to authenticate the user
        user = authenticate(username=username, password=password)
        # Check for authentication (valid username & password) AND for permissions
        if not user or not user.has_perm(perms):
            return False
        # User is authenticated AND authorized
        return user

    return inner
from myproject.myapp.auth import check_view_permissions, forbid_bots_access


@server.register_procedure(auth=check_view_permissions("auth.view_user"))
def my_rpc_method_with_builtin_predicate(username: str):
    user = User.objects.get(username=username).
    return f"{u.first_name} {u.last_name}"

@server.register_procedure(auth=forbid_bots_access)
def my_rpc_method_with_custom_authentication(a, b):
    return a + b