Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions jsonapi_requests/orm/deferred_auth_api_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from typing import Callable

from jsonapi_requests.orm import ApiModel, OrmApi
from jsonapi_requests.orm.api_model import OptionsFactory, Options, ApiModelMetaclass


class JSONAPIClientNotFound(KeyError):
pass


class DeferredAuthOptionsFactory(OptionsFactory):
"""API Options Factory connected to flask.g for jsonapi client fetch."""

def __init__(self, klass, klass_attrs):
super().__init__(klass, klass_attrs)
self._api_builder = None

def get(self):
api = self.api_builder()
return Options(type=self.type, api=api, fields=self.fields, path=self.path)

@property
def api_builder(self) -> Callable[..., OrmApi]:
return self._api_builder

@api_builder.setter
def api_builder(self, api_builder: Callable[..., OrmApi]):
self._api_builder = api_builder


class DeferredAuthOptionsFactoryMetaclass(type):
def __init__(cls, name, bases, attrs):
super().__init__(name, bases, attrs)


class DeferredAuthAPIModelMetaclass(ApiModelMetaclass, type):
"""A Metaclass enabling complex querying of JSONAPI resources, building API config options later than class def."""

def __init__(metacls, cls, bases, classdict):
"""Defer options construction to a later stage than class definition.

Args:
metacls: this metaclass instance
cls: the class name for the class to create
bases: the base classes for the class to create
classdict: all the attributes and methods to put inside the __dict__
"""
super().__init__(cls, bases, classdict)
metacls.classdict = classdict
metacls._options = None

def __call__(self, *args, **kwargs):
"""Defer API configuration at class instantiation time and not definition time.

Args:
args : tuple, Position only arguments of the new class
kwargs : dict, Keyword only arguments of the new class

"""
instance = DeferredAuthApiModel.__new__(self)
DeferredAuthApiModel.__init__(instance, *args, **kwargs)
if not hasattr(instance, "api"):
raise NotImplementedError(
f"Instance of class {instance.__class__.__name__} is derived from"
f" metaclass {self.__class__.__name__}. "
"Therefore it is required to implement interface classmethod"
f" `{instance.__class__.__name__}.api`."
)
_options_factory = DeferredAuthOptionsFactory(self, self.classdict)
_options_factory.api_builder = instance.api
self._options = _options_factory.get()
if self._options.api and self._options.type:
self._options.api.type_registry.register(instance.__class__)
return instance


class DeferredAuthApiModel(ApiModel, metaclass=DeferredAuthAPIModelMetaclass):
"""An ORM Model for PR-API resources objects, able to build complex JSONAPI queries."""

@classmethod
def api(cls) -> OrmApi:
raise NotImplementedError(
"Any class derived from metaclass DeferredAuthAPIModelMetaclass should implement api classmethod."
)
35 changes: 35 additions & 0 deletions jsonapi_requests/orm/flask/api_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Optional

from jsonapi_requests import Api
from jsonapi_requests.auth import FlaskForwardAuth
from jsonapi_requests.orm import OrmApi
from jsonapi_requests.orm.deferred_auth_api_model import DeferredAuthApiModel


class FlaskAuthApiModel(DeferredAuthApiModel):
@classmethod
def api(cls) -> OrmApi:
return OrmApi(
Api.config({
"API_ROOT": cls.api_root(),
"AUTH": cls.auth(),
"VALIDATE_SSL": cls.validate_ssl(),
"TIMEOUT": cls.timeout(),
})
)

@classmethod
def auth(cls):
return FlaskForwardAuth()

@classmethod
def timeout(cls) -> Optional[int]:
raise NotImplementedError()

@classmethod
def validate_ssl(cls) -> bool:
raise NotImplementedError()

@classmethod
def api_root(cls) -> str:
raise NotImplementedError()
95 changes: 95 additions & 0 deletions tests/test_deferred_auth_orm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from typing import Optional
from unittest import mock

import pytest
from flask import Flask
from jsonapi_requests import data

from jsonapi_requests.orm import AttributeField, RelationField

from jsonapi_requests.orm.deferred_auth_api_model import DeferredAuthApiModel
from jsonapi_requests.orm.flask.api_model import FlaskAuthApiModel


@pytest.fixture
def flask_app():
app = Flask(__name__)
yield app


@pytest.fixture
def valid_response():
response = mock.Mock(status_code=200)
response.json.return_value = {
"data": data.JsonApiObject(type="person", id="123", attributes={"first-name": "alice"}).as_data()
}
return response


@pytest.fixture
def request_send_mock(valid_response):
with mock.patch("requests.sessions.Session.send") as mocked:
mocked.return_value = valid_response
yield mocked


class FlaskClientToRailsServer(FlaskAuthApiModel):
@classmethod
def timeout(cls) -> Optional[int]:
return None

@classmethod
def validate_ssl(cls) -> bool:
return False

@classmethod
def api_root(cls) -> str:
return "http://some.rails/api/endpoint/"


class TestDeferredAuthApiModel:
def test_class_definition_without_api_meta_attribute(self):
class Person(DeferredAuthApiModel):
class Meta:
type = "person"
path = "patients"

first_name = AttributeField("first-name")

def test_class_instantiation_without_api_defined_method_raises_not_implemented(self):
class Person(DeferredAuthApiModel):
class Meta:
type = "person"
path = "patients"

first_name = AttributeField("first-name")

with pytest.raises(NotImplementedError):
Person("123")


class TestFlaskAuthApiModel:
def test_class_definition_without_api_meta_attribute(self):
class Person(FlaskAuthApiModel):
class Meta:
type = "person"
path = "http://some/api/endpoint/patients"

first_name = AttributeField("first-name")

def test_get_request(self, flask_app, request_send_mock):
class Person(FlaskClientToRailsServer):
class Meta:
type = "person"
path = "persons"

first_name = AttributeField("first-name")

with flask_app.app_context():
with flask_app.test_request_context(
headers={"Authorization": "Bearer 11111111-1111-1111-1111-111111111111"}
):
_ = Person.from_id("123").first_name
args, kwargs = request_send_mock.call_args
headers = args[0].headers
assert "Bearer 11111111-1111-1111-1111-111111111111" in headers["Authorization"]