-
Notifications
You must be signed in to change notification settings - Fork 9
Project status? #23
Comments
I believe project idea is valuable. But current implementation is not solving the problem it aims to fix: One particular thing I dislike is that it forces user to write classes. It seems it sits between the user and pyramid framework by settings weird opinionated conventions. While I was in my flight from YUL to CDG last night, I was not able to sleep and came up with another way. Example: import royal
from pyramid.authentication import RemoteUserAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.config import Configurator
from pyramid.renderers import JSON
from pyramid.security import ALL_PERMISSIONS, Allow, Everyone
def main(_, **settings):
config = Configurator(settings=settings)
config.include(".")
return config.make_wsgi_app()
def includeme(config):
config.include("royal")
config.set_authentication_policy(RemoteUserAuthenticationPolicy())
config.set_authorization_policy(ACLAuthorizationPolicy())
config.add_renderer(None, JSON()) # None = default renderer
config.scan()
collection = royal.resource("/users")
@collection.acl
def __acl__(request):
return [(Allow, "admin", ("get", "post"))]
@collection.get
def get_collection(request):
return "get_collection"
@collection.post
def create_item(request):
return "create_item"
item = royal.resource("/users/{user_id}")
@item.acl
def __acl__(request):
user_id = request.matchdict["user_id"]
return [(Allow, f"user:{user_id}", "get")]
@item.get
def get_item(request):
return "get_item" Implementation: from dataclasses import dataclass
from typing import Callable, List, Tuple, Type, TypeVar
from pyramid.config import Configurator
from pyramid.request import Request
import venusian
_category = "Royal resources - 👑"
def includeme(config: Configurator):
config.add_directive("add_resource", add_resource, action_wrap=True)
config.add_directive("add_resource_acl", add_resource_acl, action_wrap=True)
config.add_directive("add_resource_view", add_resource_view, action_wrap=True)
class _Context:
def __init__(self, request):
self.request = request
def __acl__(self, request):
raise NotImplementedError()
C = TypeVar("C", bound=_Context)
@dataclass
class Resource:
path: str
kwargs: dict
context_factory: Type[C] = None
def acl(self, acl_callable: Callable[[Request], List[Tuple[str, str, str]]]):
def callback(context, name, action):
config = context.config.with_package(info.module)
config.add_resource_acl(self, acl_callable, **settings)
info = venusian.attach(acl_callable, callback)
settings = {"_info": info.codeinfo}
self.context_factory = type(
"Context",
tuple([_Context]),
{"__acl__": lambda self: acl_callable(self.request)},
)
return acl_callable
def get(self, view: Callable = None, **kwargs):
return view_decorator(self, "GET", view, **kwargs)
def post(self, view: Callable = None, **kwargs):
return view_decorator(self, "POST", view, **kwargs)
def patch(self, view: Callable = None, **kwargs):
return view_decorator(self, "PATCH", view, **kwargs)
def put(self, view: Callable = None, **kwargs):
return view_decorator(self, "PUT", view, **kwargs)
def delete(self, view: Callable = None, **kwargs):
return view_decorator(self, "DELETE", view, **kwargs)
def resource(path: str, **kwargs) -> Resource:
def callback(context, name, action):
config = context.config.with_package(info.module)
config.add_resource(resource, **settings)
resource = Resource(path, kwargs)
info = venusian.attach(resource, callback)
settings = {"_info": info.codeinfo}
return resource
def view_decorator(
resource: Resource, request_method: str, view: Callable = None, **kwargs
):
wrapper = wrap_view(resource, request_method, **kwargs)
# allows for optional decorator args
if view is None:
return wrapper
else:
return wrapper(view, depth=3)
class wrap_view:
def __init__(self, resource: Resource, request_method: str, **kwargs):
self.resource = resource
self.request_method = request_method
self.kwargs = kwargs
def __call__(self, view: Callable, depth=1):
def callback(context, name, action):
config = context.config.with_package(info.module)
config.add_resource_view(
self.resource, self.request_method, view, self.kwargs, **settings
)
info = venusian.attach(view, callback, depth=depth)
settings = {"_info": info.codeinfo}
return view
def add_resource(config: Configurator, resource: Resource):
def add_route():
config.add_route(
name=resource.path,
pattern=resource.path,
factory=resource.context_factory,
**resource.kwargs,
)
introspectable = config.introspectable(
category_name=_category,
discriminator=(__name__, "resource", resource.path),
title=resource.path,
type_name="resource:",
)
config.action(
introspectable.discriminator,
callable=add_route,
order=-10,
introspectables=(introspectable,),
)
def add_resource_acl(config, resource, acl_callable):
introspectable = config.introspectable(
category_name=_category,
discriminator=(__name__, "acl", resource.path),
title=f"{resource.path}",
type_name="acl:",
)
config.action(
introspectable.discriminator, None, order=-20, introspectables=(introspectable,)
)
def add_resource_view(
config: Configurator,
resource: Resource,
request_method: str,
view: Callable,
kwargs,
):
introspectable = config.introspectable(
category_name=_category,
discriminator=(__name__, "view", resource.path, request_method),
title=f"{request_method} {resource.path}",
type_name="view:",
)
config.action(introspectable.discriminator, None, introspectables=(introspectable,))
permission = request_method.lower() if resource.context_factory else None
config.add_view(
route_name=resource.path,
view=view,
request_method=request_method,
permission=permission,
**kwargs,
) I did not have internet so ... it is just a proof of concept with no more usage of traversal: hence acl have to be defined on all resource if needed. |
Short answer: I do like the class idioms. It looks like stock Pyramid’s class views, only with different decorators than view_config. Having to learn and use another way of using decorators on loose functions looks like… something different for no developer benefit? |
I agree with you. Also, I am far from being motivated to work on this anyway. :-( |
I’ve been doing a small personal project recently, using it as a playground to test Pyramid 2 (authn and authz policies are replaced by a security policy), some frontend stuff, devbuddy as command runner, mercurial as VCS with good UI, etc. I use traversal, with simple context classes that represent collections and resources (but not any base class or wrapper or decorator), and views defined in another module using I don’t mind writing classes with When I was in the train without internet, I implemented a So I think that this project would be super useful if it let people follow Pyramid ways (classes to traverse, |
Hey @hadrien, long time no see 😎
Do you think this project still has value (convention-over-configuration scaffolding for RESTful web services on top of Pyramid), or that truly good generic lib can’t be done and each project should define its mini-framework (like the one we both know that parses swagger spec to derive request validation schema)?
The text was updated successfully, but these errors were encountered: