Skip to content

Commit

Permalink
Merge pull request #99 from cloudblue/LITE-28127
Browse files Browse the repository at this point in the history
LITE-28127: added django filter and pagination utils functions
  • Loading branch information
Sainomori authored Nov 20, 2023
2 parents c9b89fa + db9dab4 commit 35034d5
Show file tree
Hide file tree
Showing 13 changed files with 1,007 additions and 246 deletions.
1 change: 1 addition & 0 deletions connect/eaas/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
DEVOPS_PAGES_ATTR_NAME = '_eaas_devops_pages'
PROXIED_CONNECT_API_ATTR_NAME = '_eaas_proxied_connect_api'
CUSTOMER_PAGES_ATTR_NAME = '_eaas_customer_pages'
DJANGO_SECRET_KEY_VAR_ATTR_NAME = '_eaas_django_secret_key_var'


PROXIED_CONNECT_API_ENDPOINTS_MAX_ALLOWED_NUMBER = 100
Expand Down
Empty file.
Empty file.
152 changes: 152 additions & 0 deletions connect/eaas/core/contrib/django/rql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from typing import Callable
from urllib.parse import unquote

from dj_rql.filter_cls import RQLFilterClass
from dj_rql.transformer import RQLLimitOffsetTransformer
from django.db.models.query import QuerySet
from fastapi import Depends, Request
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from lark.exceptions import LarkError
from py_rql.exceptions import RQLFilterParsingError
from py_rql.parser import RQLParser


class _RQLLimitOffsetPaginator:

def __init__(self, query, default_limit):
self.query = query
self.default_limit = default_limit

def extract_limit_offset_from_rql(self):
rql_ast = RQLParser.parse_query(self.query)
try:
self._rql_limit, self._rql_offset = RQLLimitOffsetTransformer().transform(rql_ast)
except LarkError:
raise RQLFilterParsingError(
details={
'error': 'Limit and offset are set incorrectly.',
},
)

def serialize(self, queryset, model):
self.extract_limit_offset_from_rql()
self.limit = self.sanitize_limit()
self.count = self.get_count(queryset)
if self.limit == 0:
self.offset = 0
body = []
else:
self.offset = self.sanitize_offset()
if self.count == 0 or self.offset > self.count:
body = []
else:
if self.limit + self.offset > self.count:
self.limit = self.count - self.offset
body = [
dict(jsonable_encoder(model.from_orm(item)))
for item in queryset[self.offset: self.offset + self.limit]
]

return JSONResponse(
body,
headers=self.get_content_range_header(body),
)

async def aserialize(self, queryset, model):
self.extract_limit_offset_from_rql()
self.limit = self.sanitize_limit()
self.count = await self.aget_count(queryset)
if self.limit == 0:
self.offset = 0
body = []
else:
self.offset = self.sanitize_offset()
if self.count == 0 or self.offset > self.count:
body = []
else:
if self.limit + self.offset > self.count:
self.limit = self.count - self.offset
body = [
dict(jsonable_encoder(model.from_orm(item)))
async for item in queryset[self.offset: self.offset + self.limit]
]

return JSONResponse(
body,
headers=self.get_content_range_header(body),
)

def get_content_range_header(self, data):
length = len(data) - 1 if data else 0
content_range = 'items {0}-{1}/{2}'.format(
self.offset,
self.offset + length,
self.count,
)
return {'Content-Range': content_range}

def sanitize_limit(self):
if self._rql_limit is not None:
try:
return self.positive_int(self._rql_limit, strict=False, cutoff=1000)
except ValueError:
pass
return self.default_limit

def get_count(self, queryset):
try:
return queryset.count()
except (AttributeError, TypeError):
return len(queryset)

async def aget_count(self, queryset):
try:
return await queryset.acount()
except (AttributeError, TypeError):
return len(queryset)

def sanitize_offset(self):
if self._rql_offset is not None:
try:
return self.positive_int(self._rql_offset)
except ValueError:
pass
return 0

def positive_int(self, integer_string, strict=False, cutoff=None):
ret = int(integer_string)
if ret < 0 or (ret == 0 and strict):
raise ValueError()
if cutoff:
return min(ret, cutoff)
return ret


def RQLFilteredQuerySet(
filter_cls: RQLFilterClass,
base_queryset: Callable[[], QuerySet] = None,
):
def _wrapper(
request: Request,
) -> QuerySet:

query = unquote(request.scope.get('query_string', b'').decode())
queryset = base_queryset() if base_queryset else filter_cls.MODEL.objects.all()
filter_instance = filter_cls(queryset)
_, qs = filter_instance.apply_filters(query)
return qs
return Depends(_wrapper)


def RQLLimitOffsetPaginator(
default_limit=100,
):
def _wrapper(
request: Request,
):
return _RQLLimitOffsetPaginator(
unquote(request.scope.get('query_string', b'').decode()),
default_limit,
)
return Depends(_wrapper)
15 changes: 15 additions & 0 deletions connect/eaas/core/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ANVIL_KEY_VAR_ATTR_NAME,
CUSTOMER_PAGES_ATTR_NAME,
DEVOPS_PAGES_ATTR_NAME,
DJANGO_SECRET_KEY_VAR_ATTR_NAME,
EVENT_INFO_ATTR_NAME,
MODULE_PAGES_ATTR_NAME,
PROXIED_CONNECT_API_ATTR_NAME,
Expand Down Expand Up @@ -625,6 +626,20 @@ def wrapper(cls):
return wrapper


def django_secret_key_variable(name):
def wrapper(cls):
setattr(cls, DJANGO_SECRET_KEY_VAR_ATTR_NAME, name)
variables = []
if not hasattr(cls, VARIABLES_INFO_ATTR_NAME):
setattr(cls, VARIABLES_INFO_ATTR_NAME, variables)
else:
variables = getattr(cls, VARIABLES_INFO_ATTR_NAME)
if len(list(filter(lambda x: x['name'] == name, variables))) == 0:
variables.append({'name': name, 'initial_value': 'changeme!', 'secure': True})
return cls
return wrapper


router = InferringRouter()
web_app = cbv
"""
Expand Down
11 changes: 10 additions & 1 deletion connect/eaas/core/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ANVIL_KEY_VAR_ATTR_NAME,
CUSTOMER_PAGES_ATTR_NAME,
DEVOPS_PAGES_ATTR_NAME,
DJANGO_SECRET_KEY_VAR_ATTR_NAME,
EVENT_INFO_ATTR_NAME,
MODULE_PAGES_ATTR_NAME,
PROXIED_CONNECT_API_ATTR_NAME,
Expand Down Expand Up @@ -80,6 +81,14 @@ def get_variables(cls) -> dict:
"""
return getattr(cls, VARIABLES_INFO_ATTR_NAME, [])

@classmethod
def get_django_secret_key_variable(cls) -> str:
"""
Returns the name of the environment variable that
stores the Anvil Server Uplink key.
"""
return getattr(cls, DJANGO_SECRET_KEY_VAR_ATTR_NAME, None)


class InstallationAdminClientMixin:
def get_installation_admin_client(self, installation_id):
Expand Down Expand Up @@ -284,7 +293,7 @@ def get_anvil_key_variable(cls) -> str:
Returns the name of the environment variable that
stores the Anvil Server Uplink key.
"""
return getattr(cls, ANVIL_KEY_VAR_ATTR_NAME, [])
return getattr(cls, ANVIL_KEY_VAR_ATTR_NAME, None)

@classmethod
def get_anvil_callables(cls):
Expand Down
Loading

0 comments on commit 35034d5

Please sign in to comment.