Compare commits
11 Commits
0.0.1.post
...
0.0.1.post
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04ef407a6f | ||
|
|
4dc7243900 | ||
|
|
4cc5080838 | ||
|
|
cffa209c9a | ||
|
|
f00cf7b4b2 | ||
|
|
346ff649ec | ||
|
|
c3ff00e877 | ||
|
|
2251b1d5e9 | ||
|
|
f9b016d845 | ||
|
|
5ce727e60c | ||
|
|
6a554af8f8 |
@@ -1,3 +1,7 @@
|
||||
eclipse.preferences.version=1
|
||||
encoding//src/pyrestresource/__init__.py=utf-8
|
||||
encoding//src/pyrestresource/__metadata__.py=utf-8
|
||||
encoding//src/pyrestresource/rest_login.py=utf-8
|
||||
encoding//src/pyrestresource/rest_resource.py=utf-8
|
||||
encoding//src/pyrestresource/rest_resource_handler_walker.py=utf-8
|
||||
encoding/<project>=UTF-8
|
||||
|
||||
@@ -6,13 +6,9 @@
|
||||
# You should have received a copy of the license along with this
|
||||
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y python3.9 python3-virtualenv python3-pip git python3.9-venv weasyprint
|
||||
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --upgrade virtualenv
|
||||
RUN python3 -m pip install --upgrade setuptools wheel build
|
||||
RUN apt install -y python3.11 python3-virtualenv python3-pip git python3-venv weasyprint
|
||||
@@ -34,6 +34,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
'packaging',
|
||||
'typeguard',
|
||||
'pydantic>=2.4,<3',
|
||||
'uvicorn>=0.23'
|
||||
]
|
||||
|
||||
@@ -19,10 +19,8 @@ from typing import TYPE_CHECKING
|
||||
from .__metadata__ import __version__, __Summuary__, __Name__
|
||||
|
||||
|
||||
from .rest_resource import (
|
||||
register_rest_rootpoint,
|
||||
RestResourceBase,
|
||||
)
|
||||
from .rest_resource import RestResourceBase
|
||||
from .rest_resource_rootpoint import register_rest_rootpoint
|
||||
|
||||
from .rest_types import rsrc_verb, T_SupportedRESTFields
|
||||
|
||||
@@ -53,3 +51,19 @@ from .rest_resource_plugin import (
|
||||
ResourcePlugin_RestResourceBase_default,
|
||||
ResourcePlugin_dict_default,
|
||||
)
|
||||
from .rest_ACL import ACL_target_user, ACL_target_group, ACL_target_group_Any, ACL_record, ACL_rule
|
||||
from .rest_login import (
|
||||
RestResourceBaseLogin,
|
||||
UserLogin,
|
||||
)
|
||||
|
||||
from .rest_exceptions import (
|
||||
RestResourceException,
|
||||
RestResourceLoginException,
|
||||
RestResourceLoginException_SessionTimeout,
|
||||
RestResourceLoginException_ClientChange,
|
||||
RestResourceLoginException_InvalidSession,
|
||||
RestResourcePluginException,
|
||||
RestResourcePluginException_InvalidPluginSignature,
|
||||
RestResourceHandlerException_Forbiden,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import UUID
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from .rest_types import T_Gen_DictKeys
|
||||
|
||||
@@ -15,3 +17,24 @@ class _JSONEncoder(json.JSONEncoder):
|
||||
# if the obj is uuid, we simply return the value of uuid
|
||||
return str(o)
|
||||
return json.JSONEncoder.default(self, o)
|
||||
|
||||
|
||||
def parse_dict_cookies(cookies: str) -> dict[str, str]:
|
||||
result = {}
|
||||
for item in cookies.split(";"):
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
if "=" not in item:
|
||||
result[item] = None
|
||||
continue
|
||||
name, value = item.split("=", 1)
|
||||
result[name] = value
|
||||
return result
|
||||
|
||||
|
||||
def forward_exception(e: Exception, forward: bool) -> None:
|
||||
if forward:
|
||||
raise e from None
|
||||
else:
|
||||
traceback.print_exc()
|
||||
|
||||
45
src/pyrestresource/rest_ACL.py
Normal file
45
src/pyrestresource/rest_ACL.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
from enum import Enum, auto
|
||||
|
||||
from .rest_types import rsrc_verb
|
||||
|
||||
if TYPE_CHECKING is True:
|
||||
from .rest_login import UserLogin
|
||||
|
||||
|
||||
class ACL_target(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class ACL_target_user(ACL_target):
|
||||
name: str
|
||||
|
||||
@classmethod
|
||||
def from_user_login(cls, user_login: UserLogin) -> ACL_target_user:
|
||||
return cls(name=user_login.username)
|
||||
|
||||
|
||||
class ACL_target_user_Annonymous(ACL_target):
|
||||
name: str = "__ANNONYMOUS__"
|
||||
|
||||
|
||||
class ACL_target_group(ACL_target):
|
||||
name: str
|
||||
|
||||
|
||||
class ACL_target_group_Any(ACL_target_group):
|
||||
name: str = "__ANY__"
|
||||
|
||||
|
||||
class ACL_rule(Enum):
|
||||
ALLOW = auto()
|
||||
DENY = auto()
|
||||
|
||||
|
||||
class ACL_record(BaseModel):
|
||||
verbs: list[rsrc_verb]
|
||||
target: ACL_target
|
||||
rule: ACL_rule
|
||||
58
src/pyrestresource/rest_exceptions.py
Normal file
58
src/pyrestresource/rest_exceptions.py
Normal file
@@ -0,0 +1,58 @@
|
||||
class RestResourceException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceModelException(RestResourceException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceModelException_ACL(RestResourceModelException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceHandlerException(RestResourceException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceHandlerException_ResourceNotFound(RestResourceHandlerException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceHandlerException_MethodNotAllowed(RestResourceHandlerException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceHandlerException_BadRequest(RestResourceHandlerException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceHandlerException_Forbiden(RestResourceHandlerException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceLoginException(RestResourceException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceLoginException_SessionTimeout(RestResourceLoginException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceLoginException_ClientChange(RestResourceLoginException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceLoginException_InvalidSession(RestResourceLoginException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceLoginException_InvalidCredentials(RestResourceLoginException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourcePluginException(RestResourceException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourcePluginException_InvalidPluginSignature(RestResourcePluginException):
|
||||
pass
|
||||
130
src/pyrestresource/rest_login.py
Normal file
130
src/pyrestresource/rest_login.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# pyrestresource(c) by chacha
|
||||
#
|
||||
# pyrestresource is licensed under a
|
||||
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
|
||||
#
|
||||
# You should have received a copy of the license along with this
|
||||
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
|
||||
|
||||
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
|
||||
|
||||
"""CLI interface module"""
|
||||
from __future__ import annotations
|
||||
from typing import Optional, ClassVar, TYPE_CHECKING
|
||||
|
||||
from secrets import token_hex, compare_digest
|
||||
from datetime import datetime, timedelta
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .rest_types import rsrc_verb
|
||||
from .rest_resource import RestResourceBase
|
||||
from .rest_ACL import ACL_record, ACL_target_group_Any, ACL_rule, ACL_target_user
|
||||
from .rest_resource_plugin import ResourcePlugin_RestResourceBase_default
|
||||
from .rest_exceptions import (
|
||||
RestResourceLoginException_InvalidCredentials,
|
||||
RestResourceLoginException_ClientChange,
|
||||
RestResourceLoginException_SessionTimeout,
|
||||
RestResourceLoginException_InvalidSession,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING is True:
|
||||
from .rest_request import RestRequest, RestRequestParams_GET
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
secret: str
|
||||
|
||||
|
||||
class UserSession(BaseModel):
|
||||
last_update: datetime
|
||||
user_login: UserLogin
|
||||
client: Optional[tuple[str, int]]
|
||||
|
||||
|
||||
class ResourcePlugin_Login(ResourcePlugin_RestResourceBase_default):
|
||||
ar_UserLogin: list[UserLogin] = []
|
||||
|
||||
def handle_resource_get(self, resource: Login, params: RestRequestParams_GET) -> Login:
|
||||
return Login(username=self.get_user_login())
|
||||
|
||||
def handle_resource_put(self, resource: Login, params: RestRequestParams_GET) -> Login:
|
||||
token = self.user_login(resource.username, resource.secret)
|
||||
self.set_resp_cookie_value("Authorization", f"Bearer {token}")
|
||||
return resource
|
||||
|
||||
|
||||
class Login(RestResourceBase):
|
||||
username: Optional[str] = Field(None)
|
||||
secret: Optional[str] = Field(
|
||||
None,
|
||||
exclude=True,
|
||||
ACL=[
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.ALLOW),
|
||||
ACL_record(verbs=[rsrc_verb.GET], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class RestResourceBaseLogin(RestResourceBase):
|
||||
_ar_user_login: ClassVar[list[UserLogin]] = []
|
||||
_ar_user_session: dict[str, UserSession] = {}
|
||||
_max_session_inactive: ClassVar[timedelta] = timedelta(minutes=20)
|
||||
_max_session_time: ClassVar[timedelta] = timedelta(hours=12)
|
||||
login: Login = Field(default=Login(), plugin=ResourcePlugin_Login)
|
||||
|
||||
def get_new_cookie_expiration_date(self) -> datetime:
|
||||
return datetime.now() + self._max_session_time
|
||||
|
||||
def _process_request_session(self, request: RestRequest) -> None:
|
||||
# print(f"[TRACE] {type(self).__name__}->_process_request_session()")
|
||||
# print(f"[TRACE] request: {id(request)}")
|
||||
auth_cookie = request.get_cookie("Authorization")
|
||||
if auth_cookie != None:
|
||||
if auth_cookie in self._ar_user_session:
|
||||
# print(f"SESSION FOUND for {request.get_client()}")
|
||||
|
||||
if self._ar_user_session[auth_cookie].client != request.get_client():
|
||||
del self._ar_user_session[auth_cookie]
|
||||
raise RestResourceLoginException_ClientChange()
|
||||
|
||||
time_diff = datetime.now() - self._ar_user_session[auth_cookie].last_update
|
||||
if time_diff > self._max_session_inactive:
|
||||
del self._ar_user_session[auth_cookie]
|
||||
raise RestResourceLoginException_SessionTimeout()
|
||||
|
||||
request.set_user(ACL_target_user(name=self._ar_user_session[auth_cookie].user_login.username))
|
||||
# print("SESSION RECOVERED")
|
||||
return
|
||||
|
||||
raise RestResourceLoginException_InvalidSession()
|
||||
return
|
||||
|
||||
# print(f"non-connected user {request.get_client()}")
|
||||
|
||||
def user_login(self, user_name: str, user_secret: str, request: RestRequest) -> str:
|
||||
already_failed: bool = False
|
||||
|
||||
for iter_user_login in self._ar_user_login:
|
||||
username_ok: bool = compare_digest(user_name, iter_user_login.username)
|
||||
secret_ok: bool = compare_digest(user_secret, iter_user_login.secret)
|
||||
|
||||
if username_ok is True:
|
||||
if secret_ok is True and not already_failed:
|
||||
return self._register_user_session(iter_user_login, request)
|
||||
else:
|
||||
already_failed = True
|
||||
else:
|
||||
pass
|
||||
pass
|
||||
|
||||
if already_failed:
|
||||
raise RestResourceLoginException_InvalidCredentials()
|
||||
|
||||
def _register_user_session(self, user_login: UserLogin, request: RestRequest) -> str:
|
||||
token = token_hex(16)
|
||||
new_user_session = UserSession(last_update=datetime.now(), user_login=user_login, client=request.get_client())
|
||||
self._ar_user_session[f"Bearer {token}"] = new_user_session
|
||||
return token
|
||||
@@ -3,16 +3,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Optional,
|
||||
Any,
|
||||
Generic,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from re import sub
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from pydantic import BaseModel, Field
|
||||
from typeguard import check_type
|
||||
|
||||
|
||||
from .rest_types import rsrc_verb, T_SupportedRESTFields
|
||||
|
||||
from .rest_types import rsrc_verb, T_AllSupportedFields
|
||||
from .rest_request_opt import (
|
||||
RestRequestParams_POST,
|
||||
RestRequestParams_DELETE,
|
||||
@@ -24,6 +25,13 @@ from .rest_request_opt import (
|
||||
_T_RestRequestParams_GET,
|
||||
_T_RestRequestParams_PUT,
|
||||
)
|
||||
from .rest_ACL import ACL_target_user, ACL_target_user_Annonymous, ACL_target_group
|
||||
from .helpers import parse_dict_cookies
|
||||
|
||||
if TYPE_CHECKING is True:
|
||||
from typing import Optional
|
||||
from .rest_types import T_SupportedRESTFields
|
||||
from .rest_resource import RestResourceBase
|
||||
|
||||
|
||||
class RequestFactory(
|
||||
@@ -42,7 +50,9 @@ class RequestFactory(
|
||||
cls_RestRequestParams_POST: type[RestRequestParams_POST] = Field(default=RestRequestParams_POST)
|
||||
cls_RestRequestParams_DELETE: type[RestRequestParams_DELETE] = Field(default=RestRequestParams_DELETE)
|
||||
|
||||
def get_RestRequest(self, url: str, verb: rsrc_verb, data: dict, query_string: Optional[str] = None) -> RestRequest:
|
||||
def get_RestRequest(
|
||||
self, root_resource: RestResourceBase, url: str, verb: rsrc_verb, data: dict, query_string: Optional[str] = None
|
||||
) -> RestRequest:
|
||||
"""get a RestRequets instance based on LUT_verb configuration
|
||||
|
||||
Args:
|
||||
@@ -53,31 +63,33 @@ class RequestFactory(
|
||||
|
||||
# /!\ mypy seems not being able to propagate typevar to composed classes
|
||||
if verb is rsrc_verb.GET:
|
||||
return RestRequest[RestRequestParams_GET](self.cls_RestRequestParams_GET, url, verb, data, query_string)
|
||||
return RestRequest[RestRequestParams_GET](self.cls_RestRequestParams_GET, root_resource, url, verb, data, query_string)
|
||||
if verb is rsrc_verb.PUT:
|
||||
return RestRequest[RestRequestParams_PUT](self.cls_RestRequestParams_PUT, url, verb, data, query_string)
|
||||
return RestRequest[RestRequestParams_PUT](self.cls_RestRequestParams_PUT, root_resource, url, verb, data, query_string)
|
||||
if verb is rsrc_verb.POST:
|
||||
return RestRequest[RestRequestParams_POST](self.cls_RestRequestParams_POST, url, verb, data, query_string)
|
||||
return RestRequest[RestRequestParams_POST](self.cls_RestRequestParams_POST, root_resource, url, verb, data, query_string)
|
||||
if verb is rsrc_verb.DELETE:
|
||||
return RestRequest[RestRequestParams_DELETE](self.cls_RestRequestParams_DELETE, url, verb, data, query_string)
|
||||
raise RuntimeError("Invalid Verb")
|
||||
return RestRequest[RestRequestParams_DELETE](self.cls_RestRequestParams_DELETE, root_resource, url, verb, data, query_string)
|
||||
raise RestResourceHandlerException_MethodNotAllowed("Invalid Verb")
|
||||
|
||||
def update_RestRequest(self, origin_request: RestRequest) -> RestRequest:
|
||||
def update_RestRequest(self, request: RestRequest) -> None:
|
||||
"""create an updated copy of a RestRequest object based on a different LUT_verb configuration
|
||||
Args:
|
||||
origin_request: the original request
|
||||
"""
|
||||
|
||||
# /!\ mypy seems not being able to propagate typevar to composed classes
|
||||
if origin_request.verb is rsrc_verb.GET:
|
||||
return RestRequest[RestRequestParams_GET](self.cls_RestRequestParams_GET, None, None, None, None, origin_request)
|
||||
if origin_request.verb is rsrc_verb.PUT:
|
||||
return RestRequest[RestRequestParams_PUT](self.cls_RestRequestParams_PUT, None, None, None, None, origin_request)
|
||||
if origin_request.verb is rsrc_verb.POST:
|
||||
return RestRequest[RestRequestParams_POST](self.cls_RestRequestParams_POST, None, None, None, None, origin_request)
|
||||
if origin_request.verb is rsrc_verb.DELETE:
|
||||
return RestRequest[RestRequestParams_DELETE](self.cls_RestRequestParams_DELETE, None, None, None, None, origin_request)
|
||||
raise RuntimeError("Invalid Verb")
|
||||
if request.verb is rsrc_verb.GET:
|
||||
request.update_ReqParams(self.cls_RestRequestParams_GET)
|
||||
elif request.verb is rsrc_verb.PUT:
|
||||
request.update_ReqParams(self.cls_RestRequestParams_PUT)
|
||||
elif request.verb is rsrc_verb.POST:
|
||||
request.update_ReqParams(self.cls_RestRequestParams_POST)
|
||||
elif request.verb is rsrc_verb.DELETE:
|
||||
request.update_ReqParams(self.cls_RestRequestParams_DELETE)
|
||||
else:
|
||||
raise RestResourceHandlerException_MethodNotAllowed("Invalid Verb")
|
||||
return
|
||||
|
||||
|
||||
class RestRequest(Generic[_T_RestRequestParams]):
|
||||
@@ -87,58 +99,51 @@ class RestRequest(Generic[_T_RestRequestParams]):
|
||||
def __init__(
|
||||
self,
|
||||
type_request_params: type[_T_RestRequestParams],
|
||||
url: Optional[str] = None,
|
||||
verb: Optional[rsrc_verb] = None,
|
||||
root_resource: RestResourceBase,
|
||||
url: str,
|
||||
verb: rsrc_verb,
|
||||
data: Optional[dict[str, T_SupportedRESTFields]] = None,
|
||||
query_string: Optional[str] = None,
|
||||
origin_request: Optional[RestRequest] = None,
|
||||
) -> None:
|
||||
"""class to handle a request context, that will be kept and updated while walking url parts
|
||||
|
||||
Args:
|
||||
type_request_params: type of the request param
|
||||
url: http url of the request
|
||||
verb: http verb received
|
||||
data: data associated with the request
|
||||
type_request_params: type of the request param
|
||||
origin_request: orginial request in case of updates.
|
||||
In this case, all other argument - but type_request_params - are ignored and inherited from the origin_request
|
||||
query_string: query arguments after url (eg: ?arg1=value1&arg2=value2 ...)
|
||||
"""
|
||||
|
||||
# defining all types
|
||||
self.url: str
|
||||
self.verb: rsrc_verb
|
||||
self.data: dict
|
||||
self._raw_headers: list[Any] = []
|
||||
self._client: tuple[str, int] = ()
|
||||
self.headers: dict[str, None | str | dict[str, None | str]] = {"host": None, "cookie": {}}
|
||||
self._saved_url_params: dict
|
||||
self.ReqParams: _T_RestRequestParams = type_request_params()
|
||||
self.url_stack: list[str]
|
||||
self._saved_url_stack: list[str]
|
||||
self.url_stack_index: int
|
||||
|
||||
# detecting Optional fields in type_request_params (to extract real type)
|
||||
# if False: # deprecated => Generic
|
||||
# if get_origin(type_request_params) is Union:
|
||||
# datatype = get_args(type_request_params)
|
||||
# if len(datatype) == 2:
|
||||
# if datatype[0] is type(None):
|
||||
# type_request_params = datatype[1]
|
||||
# elif datatype[1] is type(None):
|
||||
# type_request_params = datatype[0]
|
||||
# else:
|
||||
# raise RuntimeError("Union is only allowed to describe Optional (e.g. Union[XXX,None])")
|
||||
|
||||
# = updating request from a previous one =
|
||||
if origin_request:
|
||||
self.__dict__ = origin_request.__dict__.copy()
|
||||
if type_request_params:
|
||||
self.ReqParams = type_request_params(**self._saved_url_params)
|
||||
# print("request updated")
|
||||
return
|
||||
self.outgoing_cookie: dict[str, str] = {}
|
||||
self.user: ACL_target_user = ACL_target_user_Annonymous()
|
||||
self.groups: list[ACL_target_group] = []
|
||||
self.result: Optional[str] = None
|
||||
self._forced_status: Optional[int] = None
|
||||
self.root_resource: RestResourceBase = root_resource
|
||||
|
||||
# = or create a fresh one =
|
||||
if url is None or verb is None or data is None:
|
||||
raise RuntimeError("url and verb and data must be set")
|
||||
raise RestResourceException("url and verb and data must be set")
|
||||
self.url = url
|
||||
self.verb = verb
|
||||
|
||||
if data != {} and not check_type(data, T_AllSupportedFields):
|
||||
raise RestResourceHandlerException_BadRequest(f"Wrong data type received: {data}")
|
||||
|
||||
self.data = data
|
||||
|
||||
# parse_qs returns list[] for all keys, the command convert list to single items so pydantic can eat them :)
|
||||
@@ -156,6 +161,71 @@ class RestRequest(Generic[_T_RestRequestParams]):
|
||||
self._saved_url_stack = self.url_stack.copy()
|
||||
self.url_stack_index = 0
|
||||
|
||||
def set_resp_status(self, status: int) -> None:
|
||||
self._forced_status = status
|
||||
|
||||
def get_root_resource(self) -> RestResourceBase:
|
||||
return self.root_resource
|
||||
|
||||
def get_status(self) -> int:
|
||||
if self._forced_status is not None:
|
||||
return self._forced_status
|
||||
|
||||
if self.verb in (rsrc_verb.POST, rsrc_verb.PUT):
|
||||
return 201
|
||||
|
||||
return 200
|
||||
|
||||
def set_client(self, client: tuple[str, int]) -> None:
|
||||
self._client = client
|
||||
|
||||
def get_client(self) -> tuple[str, int]:
|
||||
return self._client
|
||||
|
||||
def set_headers(self, headers: list[Any]) -> None:
|
||||
self._raw_headers = headers
|
||||
for elem in self._raw_headers:
|
||||
if elem[0] == b"host":
|
||||
self.headers["host"] = elem[1].decode("utf-8")
|
||||
elif elem[0] == b"user-agent":
|
||||
self.headers["user-agent"] = elem[1].decode("utf-8")
|
||||
elif elem[0] == b"cookie":
|
||||
self.headers["cookie"] = parse_dict_cookies(elem[1].decode("utf-8"))
|
||||
|
||||
def get_cookie(self, key: str) -> str | None:
|
||||
if key not in self.headers["cookie"]:
|
||||
return None
|
||||
return self.headers["cookie"][key]
|
||||
|
||||
def set_resp_cookie_value(self, key: str, value: str) -> None:
|
||||
self.outgoing_cookie[
|
||||
key
|
||||
] = f"{value}; expires={self.root_resource.get_new_cookie_expiration_date().strftime('%a, %d %b %Y %H:%M:%S GMT')}; path=/; HttpOnly"
|
||||
|
||||
def reset_resp_cookie(self, key: str) -> None:
|
||||
self.outgoing_cookie[key] = "null; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
|
||||
def get_host(self) -> str:
|
||||
return self.headers["host"]
|
||||
|
||||
def set_result(self, result: str):
|
||||
self.result = result
|
||||
|
||||
def get_result(self) -> Optional[str]:
|
||||
return self.result
|
||||
|
||||
def set_user(self, user: ACL_target_user):
|
||||
self.user = user
|
||||
|
||||
def get_user(self):
|
||||
return self.user
|
||||
|
||||
def add_group(self, group: ACL_target_group):
|
||||
self.groups.append(group)
|
||||
|
||||
def update_ReqParams(self, type_request_params: type[_T_RestRequestParams]):
|
||||
self.ReqParams = type_request_params(**self._saved_url_params)
|
||||
|
||||
def _parse_url(self, url: str) -> None:
|
||||
# remove repeated slash ('/')
|
||||
url = sub(r"\/{2,}", "/", url)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Generic, Optional, TypeVar, TYPE_CHECKING
|
||||
|
||||
from typing import Optional, Generic, TypeVar
|
||||
from pydantic import BaseModel, Extra
|
||||
|
||||
from .rest_types import (
|
||||
_T_DictKey,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING is True:
|
||||
pass
|
||||
|
||||
|
||||
class RestRequestParams(BaseModel, extra=Extra.allow):
|
||||
pass
|
||||
|
||||
@@ -1,195 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# pyrestresource(c) by chacha
|
||||
#
|
||||
# pyrestresource is licensed under a
|
||||
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
|
||||
#
|
||||
# You should have received a copy of the license along with this
|
||||
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
|
||||
|
||||
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
|
||||
|
||||
"""CLI interface module"""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from typing import (
|
||||
cast,
|
||||
Any,
|
||||
ClassVar,
|
||||
get_args,
|
||||
get_origin,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from abc import ABC
|
||||
import json
|
||||
from pydantic.fields import FieldInfo
|
||||
import pprint
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .helpers import _JSONEncoder
|
||||
from .helpers import _JSONEncoder, forward_exception
|
||||
from .rest_types import rsrc_verb, _T_SupportedRESTFields
|
||||
from .rest_resource_plugin import (
|
||||
ResourcePlugin_field,
|
||||
ResourcePlugin_RestResourceBase,
|
||||
ResourcePlugin_dict,
|
||||
|
||||
from .rest_ACL import (
|
||||
ACL_record,
|
||||
ACL_target_user,
|
||||
ACL_target_group,
|
||||
ACL_target_group_Any,
|
||||
ACL_rule,
|
||||
)
|
||||
|
||||
|
||||
from .rest_resource_walker import (
|
||||
RestResourceWalkerFutureResult,
|
||||
RestResourceWalker_Root,
|
||||
RestResourceWalker_Sub_T_Dict,
|
||||
RestResourceWalker_Sub_RestFields,
|
||||
RestResourceWalker_Sub_RestResourceBase,
|
||||
from .rest_request import RestRequest
|
||||
from .rest_exceptions import (
|
||||
RestResourceLoginException_InvalidSession,
|
||||
RestResourceLoginException_SessionTimeout,
|
||||
RestResourceLoginException_ClientChange,
|
||||
RestResourceLoginException_InvalidCredentials,
|
||||
RestResourceHandlerException_ResourceNotFound,
|
||||
RestResourceHandlerException_MethodNotAllowed,
|
||||
RestResourceHandlerException_BadRequest,
|
||||
RestResourceHandlerException_Forbiden,
|
||||
RestResourceException,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING is True:
|
||||
from .rest_types import (
|
||||
T_ListIndex,
|
||||
T_ListSize,
|
||||
T_DictKey,
|
||||
T_T_DictKey,
|
||||
T_DictValues,
|
||||
T_T_DictValues,
|
||||
T_SupportedRESTFields,
|
||||
)
|
||||
|
||||
|
||||
class RestResourceWalkerFutureResult_RestResourceBase_tree_exclude(RestResourceWalkerFutureResult[dict]):
|
||||
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
|
||||
res = {}
|
||||
res[self.source.resource_name] = dict()
|
||||
for subres in result:
|
||||
key = next(iter(subres))
|
||||
if (
|
||||
key in self.source.annotation._model_dump_excluded_ # pylint: disable=protected-access
|
||||
and self.source.annotation._model_dump_excluded_[key] is True # pylint: disable=protected-access
|
||||
):
|
||||
res[self.source.resource_name] = res[self.source.resource_name] | {key: True}
|
||||
else:
|
||||
res[self.source.resource_name] = res[self.source.resource_name] | subres
|
||||
return res
|
||||
|
||||
|
||||
class RestResourceWalkerFutureResult_Dict_tree_exclude(RestResourceWalkerFutureResult[dict]):
|
||||
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
|
||||
res = {}
|
||||
for subres in result:
|
||||
res = res | subres
|
||||
return res
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_T_Dict__tree_exclude(RestResourceWalker_Sub_T_Dict):
|
||||
cls_RestResourceWalkerFutureResult = RestResourceWalkerFutureResult_Dict_tree_exclude
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_RestResourceBase__tree_exclude(RestResourceWalker_Sub_RestResourceBase):
|
||||
cls_RestResourceWalkerFutureResult = RestResourceWalkerFutureResult_RestResourceBase_tree_exclude
|
||||
|
||||
|
||||
class RestResourceWalker_Root__tree_exclude(RestResourceWalker_Root):
|
||||
cls_RestResourceWalker_Sub = [
|
||||
RestResourceWalker_Sub_T_Dict__tree_exclude,
|
||||
RestResourceWalker_Sub_RestFields,
|
||||
RestResourceWalker_Sub_RestResourceBase__tree_exclude,
|
||||
]
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_T_Dict__tree_init(RestResourceWalker_Sub_T_Dict):
|
||||
def process(self) -> None:
|
||||
datatype = get_args(self.annotation)
|
||||
|
||||
# checking compatibility
|
||||
if not get_origin(datatype[1]) is None:
|
||||
raise RuntimeError("complex dict types are not supported (should create a RestResourceBase container)")
|
||||
if not datatype[0] in _T_SupportedRESTFields:
|
||||
raise RuntimeError(f"Unsupported Dict Field value type in class (key)")
|
||||
|
||||
# preprocessing types / structure
|
||||
if self.parent is not None and isinstance(self.parent, RestResourceWalker_Sub_RestResourceBase):
|
||||
self.parent.annotation._dict_key_type_[self.resource_name] = datatype[0] # pylint: disable=protected-access
|
||||
self.parent.annotation._dict_value_type_[self.resource_name] = datatype[1] # pylint: disable=protected-access
|
||||
self.parent.annotation._model_dump_excluded_[self.resource_name] = True # pylint: disable=protected-access
|
||||
|
||||
if (
|
||||
isinstance(self.resource, FieldInfo)
|
||||
and self.resource.json_schema_extra is not None
|
||||
and type(self.resource.json_schema_extra) is dict
|
||||
and "plugin" in self.resource.json_schema_extra
|
||||
):
|
||||
plugin_dict: ResourcePlugin_dict = self.resource.json_schema_extra["plugin"]()
|
||||
if not isinstance(plugin_dict, ResourcePlugin_dict):
|
||||
raise RuntimeError("Wrong plugin signature provided")
|
||||
self.parent.annotation._plugins_[self.resource_name] = plugin_dict
|
||||
# print("ADD DICT PLUGIN")
|
||||
|
||||
else:
|
||||
raise RuntimeError("dict must be contained in a RestResourceBase")
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_RestFields__tree_init(RestResourceWalker_Sub_RestFields):
|
||||
def process(self) -> None:
|
||||
if self.parent is not None and isinstance(self.parent, RestResourceWalker_Sub_RestResourceBase):
|
||||
if (
|
||||
isinstance(self.resource, FieldInfo)
|
||||
and self.resource.json_schema_extra is not None
|
||||
and type(self.resource.json_schema_extra) is dict
|
||||
):
|
||||
if "primary_key" in self.resource.json_schema_extra and self.resource.json_schema_extra["primary_key"] is True:
|
||||
if self.parent.annotation._primary_key_ is not None:
|
||||
raise RuntimeError(f"Only one primary key is allowed {self.parent.resource_name}.{self.resource_name}")
|
||||
self.parent.annotation._primary_key_ = self.resource_name
|
||||
|
||||
if "plugin" in self.resource.json_schema_extra and self.resource.json_schema_extra["plugin"]:
|
||||
plugin_field: ResourcePlugin_field = self.resource.json_schema_extra["plugin"]()
|
||||
if not isinstance(plugin_field, ResourcePlugin_field):
|
||||
raise RuntimeError("Wrong plugin signature provided")
|
||||
self.parent.annotation._plugins_[self.resource_name] = plugin_field
|
||||
# print("ADD FIELD PLUGIN")
|
||||
|
||||
else:
|
||||
raise RuntimeError("fields must be contained in a RestResourceBase")
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_RestResourceBase__tree_init(RestResourceWalker_Sub_RestResourceBase):
|
||||
def process(self) -> None:
|
||||
setattr(self.annotation, "_dict_key_type_", {})
|
||||
setattr(self.annotation, "_dict_value_type_", {})
|
||||
setattr(self.annotation, "_model_dump_excluded_", {})
|
||||
setattr(self.annotation, "_primary_key_", None)
|
||||
setattr(self.annotation, "_plugins_", {})
|
||||
|
||||
# preprocessing types / structure
|
||||
if self.parent is not None and isinstance(self.parent, RestResourceWalker_Sub_RestResourceBase):
|
||||
self.parent.annotation._model_dump_excluded_[self.resource_name] = True
|
||||
|
||||
if (
|
||||
isinstance(self.resource, FieldInfo)
|
||||
and self.resource.json_schema_extra is not None
|
||||
and type(self.resource.json_schema_extra) is dict
|
||||
and "plugin" in self.resource.json_schema_extra
|
||||
):
|
||||
plugin_resource: ResourcePlugin_RestResourceBase = self.resource.json_schema_extra["plugin"]()
|
||||
if not isinstance(plugin_resource, ResourcePlugin_RestResourceBase):
|
||||
raise RuntimeError("Wrong plugin signature provided")
|
||||
self.parent.annotation._plugins_[self.resource_name] = plugin_resource
|
||||
# print("ADD RESOURCE PLUGIN")
|
||||
|
||||
|
||||
class RestResourceWalker_Root__tree_init(RestResourceWalker_Root):
|
||||
cls_RestResourceWalker_Sub = [
|
||||
RestResourceWalker_Sub_T_Dict__tree_init,
|
||||
RestResourceWalker_Sub_RestFields__tree_init,
|
||||
RestResourceWalker_Sub_RestResourceBase__tree_init,
|
||||
]
|
||||
|
||||
|
||||
def register_rest_rootpoint(klass: type[RestResourceBase]):
|
||||
RestResourceWalker_Root__tree_init(klass).process()
|
||||
return klass
|
||||
|
||||
|
||||
class RestResourceBase(ABC, BaseModel, validate_assignment=True):
|
||||
# _resp_cookies: ClassVar[dict[str, str]] = {}
|
||||
_dict_key_type_: ClassVar[dict[str, T_T_DictKey]] = {}
|
||||
_dict_value_type_: ClassVar[dict[str, T_T_DictValues]] = {}
|
||||
_model_dump_excluded_: ClassVar[dict[str, bool]] = {}
|
||||
@@ -197,9 +52,57 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
|
||||
_plugins_: ClassVar[
|
||||
dict[
|
||||
str,
|
||||
ResourcePlugin_field | ResourcePlugin_RestResourceBase | ResourcePlugin_dict,
|
||||
list[ACL_record],
|
||||
]
|
||||
] = {}
|
||||
_ACL_record_: ClassVar[
|
||||
dict[
|
||||
str,
|
||||
ACL_record,
|
||||
]
|
||||
] = {}
|
||||
|
||||
def _check_acl(self, user: ACL_target_user, groups: list[ACL_target_group], verb: rsrc_verb, field: str, is_self: bool = True):
|
||||
# print(f"evaluate self ACLs rule: {self._ACL_record_}")
|
||||
# print(f"user: {user}")
|
||||
# print(f"groups: {groups}")
|
||||
if is_self and verb is rsrc_verb.GET and self.model_fields[field].exclude is True:
|
||||
# print("ALLOWED (excluded field)")
|
||||
return
|
||||
for acl in self._ACL_record_[field]:
|
||||
# print(f"evaluate ACL rule: {acl}")
|
||||
if verb in acl.verbs:
|
||||
if isinstance(acl.target, ACL_target_user):
|
||||
if user == acl.target:
|
||||
if acl.rule is ACL_rule.ALLOW:
|
||||
# print("ALLOWED (user)")
|
||||
return
|
||||
raise RestResourceHandlerException_Forbiden(f"Not allowed access detected: {field}")
|
||||
elif isinstance(acl.target, ACL_target_group):
|
||||
if isinstance(acl.target, ACL_target_group_Any) or any(_ for _ in groups if _.name == acl.target.name):
|
||||
if acl.rule is ACL_rule.ALLOW:
|
||||
# print("ALLOWED (group)")
|
||||
return
|
||||
raise RestResourceHandlerException_Forbiden(f"Not allowed access detected: {field}")
|
||||
else:
|
||||
raise RestResourceException(f"Wrong ACL target type: {field}")
|
||||
# print("ALLOWED (Default)")
|
||||
|
||||
def check_acl_field(self, request: RestRequest, req_index: int = 0) -> None:
|
||||
"""Check ACL on requested field access"""
|
||||
self._check_acl(request.user, request.groups, request.get_verb(), request.get_resource_origin(req_index), False)
|
||||
|
||||
def check_acl_self(self, request: RestRequest, new_data: Optional[dict[str, _T_SupportedRESTFields]]) -> None:
|
||||
"""Check ACL on requested field operation (involving checking sub-fields)"""
|
||||
if request.get_verb() is rsrc_verb.GET:
|
||||
for key in self.model_fields.keys():
|
||||
self._check_acl(request.user, request.groups, rsrc_verb.GET, key)
|
||||
elif request.get_verb() is rsrc_verb.PUT:
|
||||
for key in new_data.keys():
|
||||
if key in self.model_fields:
|
||||
self._check_acl(request.user, request.groups, rsrc_verb.PUT, key)
|
||||
else:
|
||||
raise RestResourceException("Incompatible verb")
|
||||
|
||||
def update(self, **new_data):
|
||||
for field, value in new_data.items():
|
||||
@@ -221,33 +124,47 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
assert scope["type"] == "http"
|
||||
|
||||
method = scope["method"]
|
||||
|
||||
assert method in ["GET", "DELETE", "PUT", "POST"]
|
||||
|
||||
if b"content-type" in scope["headers"]:
|
||||
assert scope["headers"][b"content-type"] == b"application/json"
|
||||
|
||||
# pprint.pprint(scope)
|
||||
|
||||
body = await self.read_body(receive)
|
||||
verb = rsrc_verb[scope["method"]]
|
||||
result = self.process_request(
|
||||
scope["path"], rsrc_verb[scope["method"]], body.decode("utf-8"), scope["query_string"].decode("utf-8")
|
||||
|
||||
request: RestRequest = self.process_request(
|
||||
scope["path"],
|
||||
rsrc_verb[scope["method"]],
|
||||
body.decode("utf-8"),
|
||||
scope["query_string"].decode("utf-8"),
|
||||
scope["client"],
|
||||
scope["headers"],
|
||||
True,
|
||||
)
|
||||
|
||||
status = 200
|
||||
if verb in (rsrc_verb.POST, rsrc_verb.PUT):
|
||||
status = 201
|
||||
assert request is not None
|
||||
|
||||
header_resp = {
|
||||
"type": "http.response.start",
|
||||
"status": request.get_status(),
|
||||
"headers": [
|
||||
[b"content-type", b"application/json"],
|
||||
],
|
||||
}
|
||||
|
||||
for name, value in request.outgoing_cookie.items():
|
||||
header_resp["headers"].append(["Set-Cookie", f"{name}={value}"])
|
||||
|
||||
await send(header_resp)
|
||||
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"status": status,
|
||||
"headers": [
|
||||
[b"content-type", b"application/json"],
|
||||
],
|
||||
}
|
||||
)
|
||||
body = None
|
||||
if result:
|
||||
body = result.encode("utf-8")
|
||||
if request.get_result():
|
||||
body = request.get_result().encode("utf-8")
|
||||
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.body",
|
||||
@@ -255,9 +172,19 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
|
||||
}
|
||||
)
|
||||
|
||||
def _process_request_session(self, request: RestRequest) -> None:
|
||||
pass
|
||||
|
||||
def process_request(
|
||||
self, url: str, verb: rsrc_verb = rsrc_verb.GET, data_json: Optional[str] = None, query_string: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
self,
|
||||
url: str,
|
||||
verb: rsrc_verb = rsrc_verb.GET,
|
||||
data_json: Optional[str] = None,
|
||||
query_string: Optional[str] = None,
|
||||
client: Optional[tuple[str, int]] = None,
|
||||
headers: Optional[list[Any]] = None,
|
||||
http_mode: bool = False,
|
||||
) -> RestRequest:
|
||||
from .rest_resource_handler import (
|
||||
ResourceHandler,
|
||||
ResourceHandler_RestResourceBase,
|
||||
@@ -267,15 +194,55 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
|
||||
if data_json:
|
||||
data = json.loads(data_json)
|
||||
|
||||
ressource: ResourceHandler = ResourceHandler_RestResourceBase(self, url, verb, data, query_string)
|
||||
result = ressource.process_verb()
|
||||
# creating the root handler
|
||||
ressource_handler: ResourceHandler = ResourceHandler_RestResourceBase(self, url, verb, data, query_string)
|
||||
|
||||
if isinstance(result, RestResourceBase):
|
||||
exclude: Optional[dict[str, bool]] = None
|
||||
raw_exclude = RestResourceWalker_Root__tree_exclude(result).process()
|
||||
exclude = next(iter(raw_exclude.values()))
|
||||
return json.dumps(result.model_dump(mode="json", exclude=exclude))
|
||||
# preparing request & session
|
||||
request: RestRequest = ressource_handler.get_request()
|
||||
assert request is not None
|
||||
|
||||
if result is not None:
|
||||
return json.dumps(result, cls=_JSONEncoder)
|
||||
return None
|
||||
if headers is not None:
|
||||
request.set_headers(headers)
|
||||
|
||||
if client is not None:
|
||||
request.set_client(client)
|
||||
|
||||
try:
|
||||
self._process_request_session(request)
|
||||
|
||||
result = ressource_handler.process_verb()
|
||||
|
||||
if isinstance(result, RestResourceBase):
|
||||
request.set_result(json.dumps(result.model_dump(mode="json")))
|
||||
elif result is not None:
|
||||
request.set_result(json.dumps(result, cls=_JSONEncoder))
|
||||
else:
|
||||
request.set_result("null")
|
||||
|
||||
except RestResourceHandlerException_ResourceNotFound as e:
|
||||
request.set_resp_status(404)
|
||||
forward_exception(e, not http_mode)
|
||||
|
||||
except RestResourceHandlerException_MethodNotAllowed as e:
|
||||
request.set_resp_status(405)
|
||||
forward_exception(e, not http_mode)
|
||||
|
||||
except RestResourceHandlerException_BadRequest as e:
|
||||
request.set_resp_status(400)
|
||||
forward_exception(e, not http_mode)
|
||||
|
||||
except RestResourceHandlerException_Forbiden as e:
|
||||
request.set_resp_status(403)
|
||||
forward_exception(e, not http_mode)
|
||||
|
||||
except (
|
||||
RestResourceLoginException_InvalidSession,
|
||||
RestResourceLoginException_SessionTimeout,
|
||||
RestResourceLoginException_ClientChange,
|
||||
RestResourceLoginException_InvalidCredentials,
|
||||
) as e:
|
||||
request.set_resp_status(401)
|
||||
request.reset_resp_cookie("Authorization")
|
||||
forward_exception(e, not http_mode)
|
||||
|
||||
return request
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
from typing import Optional, cast, TypeVar, Generic, Self, TYPE_CHECKING
|
||||
|
||||
import abc
|
||||
|
||||
from .rest_types import (
|
||||
rsrc_verb,
|
||||
T_SupportedRESTFields,
|
||||
T_DictKey,
|
||||
_T_SupportedRESTFields,
|
||||
T_Dict,
|
||||
T_T_DictValues,
|
||||
T_DictValues,
|
||||
)
|
||||
from .rest_resource import RestResourceBase
|
||||
from .rest_request import RequestFactory, RestRequest
|
||||
|
||||
from .rest_request import RequestFactory
|
||||
from .rest_resource_plugin import (
|
||||
ResourcePlugin_field,
|
||||
ResourcePlugin_RestResourceBase,
|
||||
)
|
||||
|
||||
from .rest_request_opt import (
|
||||
RestRequestParams_POST,
|
||||
RestRequestParams_DELETE,
|
||||
@@ -35,16 +33,17 @@ from .rest_request_opt import (
|
||||
_T_RestRequestParams_PUT,
|
||||
)
|
||||
|
||||
from .rest_resource_handler_walker import RestResourceWalker_Root__handler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .rest_types import (
|
||||
T_ListIndex,
|
||||
T_ListSize,
|
||||
T_T_DictKey,
|
||||
T_FieldValue,
|
||||
)
|
||||
from .rest_exceptions import (
|
||||
RestResourceHandlerException,
|
||||
RestResourceHandlerException_ResourceNotFound,
|
||||
RestResourceHandlerException_MethodNotAllowed,
|
||||
RestResourceHandlerException_BadRequest,
|
||||
RestResourceHandlerException_Forbiden,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING is True:
|
||||
from .rest_types import T_T_DictKey, T_T_DictValues
|
||||
from .rest_request import RestRequest
|
||||
|
||||
_T_Resource = TypeVar("_T_Resource", T_DictValues, T_Dict, T_SupportedRESTFields, RestResourceBase)
|
||||
|
||||
@@ -95,15 +94,17 @@ class ResourceHandler(
|
||||
self.req: RestRequest
|
||||
if prev_handler is not None:
|
||||
self.prev_handler = prev_handler
|
||||
self.req = self._request_factory.update_RestRequest(self.prev_handler.req)
|
||||
self.req = prev_handler.get_request()
|
||||
self._request_factory.update_RestRequest(self.req)
|
||||
|
||||
elif None in [url, verb]:
|
||||
raise RuntimeError("if req not set, url,verb must be setted")
|
||||
raise RestResourceHandlerException("if req not set, url,verb must be setted")
|
||||
else:
|
||||
if url is None or verb is None:
|
||||
raise RuntimeError("url and verb must be set")
|
||||
raise RestResourceHandlerException("url and verb must be set")
|
||||
if data is None:
|
||||
data = {}
|
||||
self.req = self._request_factory.get_RestRequest(url, verb, data, query_string)
|
||||
self.req = self._request_factory.get_RestRequest(resource, url, verb, data, query_string)
|
||||
|
||||
# print(f"[TRACE] creating {type(self).__name__}() with url={self.req.get_url_stack()}")
|
||||
|
||||
@@ -122,13 +123,16 @@ class ResourceHandler(
|
||||
if resource_handler_cls._check_resource_handler(resource, req):
|
||||
# print(f"[DEBUG] match ResourceHandler: {resource_handler_cls.__name__}")
|
||||
return resource_handler_cls
|
||||
raise RuntimeError(f"Unsupported Resource Type {type(resource).__name__}")
|
||||
raise RestResourceHandlerException(f"Unsupported Resource Type {type(resource).__name__}")
|
||||
|
||||
@classmethod
|
||||
def register_resource_handler(cls, other_cls) -> None:
|
||||
cls._ar_resource_handler_cls_.append(other_cls)
|
||||
return other_cls
|
||||
|
||||
def get_request(self) -> RestRequest:
|
||||
return self.req
|
||||
|
||||
def process_verb(
|
||||
self,
|
||||
) -> Optional[_T_Resource | T_DictKey | list[T_DictKey]]:
|
||||
@@ -189,8 +193,8 @@ class ResourceHandler(
|
||||
self.next_handler = next_resource_handler
|
||||
return next_resource_handler
|
||||
|
||||
# in the context of _find_resource, only resource real values can be retrieved
|
||||
raise RuntimeError("Wrong request")
|
||||
# in _find_resource context, only resource's real values can be retrieved
|
||||
raise RestResourceHandlerException_ResourceNotFound()
|
||||
|
||||
def _check_access_rights(self):
|
||||
pass
|
||||
@@ -213,7 +217,7 @@ class ResourceHandler(
|
||||
self._process_delete()
|
||||
return None
|
||||
|
||||
raise RuntimeError("Invalid Verb")
|
||||
raise RestResourceHandlerException_BadRequest("Invalid Verb")
|
||||
|
||||
def _process_get(
|
||||
self,
|
||||
@@ -234,16 +238,16 @@ class ResourceHandler(
|
||||
self._handle_process_delete(self.req.get_req_params())
|
||||
|
||||
def _handle_process_get(self, params: _T_RestRequestParams_GET) -> _T_Resource | list[T_DictKey]:
|
||||
raise RuntimeError(f"GET method not implemented for {type(self).__name__}")
|
||||
raise RestResourceHandlerException_MethodNotAllowed(f"GET method not implemented for {type(self).__name__}")
|
||||
|
||||
def _handle_process_put(self, params: _T_RestRequestParams_PUT) -> None:
|
||||
raise RuntimeError(f"PUT method not implemented for {type(self).__name__}")
|
||||
raise RestResourceHandlerException_MethodNotAllowed(f"PUT method not implemented for {type(self).__name__}")
|
||||
|
||||
def _handle_process_post(self, params: _T_RestRequestParams_POST) -> Optional[T_DictKey]:
|
||||
raise RuntimeError(f"POST method not implemented for {type(self).__name__}")
|
||||
raise RestResourceHandlerException_MethodNotAllowed(f"POST method not implemented for {type(self).__name__}")
|
||||
|
||||
def _handle_process_delete(self, params: _T_RestRequestParams_DELETE) -> None:
|
||||
raise RuntimeError(f"DELETE method not implemented for {type(self).__name__}")
|
||||
raise RestResourceHandlerException_MethodNotAllowed(f"DELETE method not implemented for {type(self).__name__}")
|
||||
|
||||
|
||||
@ResourceHandler.register_resource_handler
|
||||
@@ -292,12 +296,11 @@ class ResourceHandler_dict(
|
||||
# print(f"{type(self).__name__}->_handle_process_delete()")
|
||||
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
|
||||
|
||||
if self.prev_handler is None:
|
||||
raise RuntimeError("Wrong command")
|
||||
assert self.prev_handler is not None
|
||||
|
||||
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(1)]
|
||||
|
||||
_dict: dict[T_DictKey, "T_DictValues"] = cast(dict[T_DictKey, "T_DictValues"], self.resource)
|
||||
_dict: dict[T_DictKey, T_DictValues] = cast(dict[T_DictKey, T_DictValues], self.resource)
|
||||
|
||||
if params.API_key is not None:
|
||||
del _dict[dict_key_type(params.API_key)]
|
||||
@@ -311,8 +314,7 @@ class ResourceHandler_dict(
|
||||
# print(f"{type(self).__name__}->_handle_process_post()")
|
||||
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
|
||||
|
||||
if self.prev_handler is None:
|
||||
raise RuntimeError("Wrong command")
|
||||
assert self.prev_handler is not None
|
||||
|
||||
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(1)]
|
||||
dict_value_type: T_T_DictValues = cast(RestResourceBase, self.prev_handler.resource)._dict_value_type_[
|
||||
@@ -321,7 +323,7 @@ class ResourceHandler_dict(
|
||||
|
||||
_obj = dict_value_type(**self.req.get_data())
|
||||
|
||||
_dict: dict[T_DictKey, "T_DictValues"] = cast(dict[T_DictKey, "T_DictValues"], self.resource)
|
||||
_dict: dict[T_DictKey, T_DictValues] = cast(dict[T_DictKey, T_DictValues], self.resource)
|
||||
|
||||
# 1st try/ using request param provided dict API_key
|
||||
if params.API_key is not None:
|
||||
@@ -344,7 +346,9 @@ class ResourceHandler_dict(
|
||||
_dict[_obj_primary_key] = _obj
|
||||
return _obj_primary_key
|
||||
|
||||
RuntimeError("Either the object needs defined primary key or the request must contain an API_key param to process this command")
|
||||
raise RestResourceHandlerException_BadRequest(
|
||||
"Either the object needs defined primary key or the request must contain an API_key param to process this command"
|
||||
)
|
||||
return None # for mypy....
|
||||
|
||||
|
||||
@@ -384,8 +388,7 @@ class ResourceHandler_dict_elem(
|
||||
# print(f"{type(self).__name__}->_process_get()")
|
||||
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
|
||||
|
||||
if self.prev_handler is None:
|
||||
raise RuntimeError("Wrong command")
|
||||
assert self.prev_handler is not None
|
||||
|
||||
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(1)]
|
||||
|
||||
@@ -404,8 +407,7 @@ class ResourceHandler_dict_elem(
|
||||
# instead of expected get_resource_origin(1) because we need to go backward
|
||||
# because self.req is another context that is not saved to improve performances
|
||||
|
||||
if self.prev_handler is None:
|
||||
raise RuntimeError("Wrong command")
|
||||
assert self.prev_handler is not None
|
||||
|
||||
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(2)]
|
||||
|
||||
@@ -455,27 +457,42 @@ class ResourceHandler_RestResourceBase(
|
||||
if self.req.get_resource_origin(0) == "/":
|
||||
return
|
||||
|
||||
if (
|
||||
self.req.get_resource_origin(0) not in self.resource.model_fields
|
||||
or self.resource.model_fields[self.req.get_resource_origin(0)].exclude is True
|
||||
):
|
||||
raise RuntimeError(f"Unknown or not allowed field access detected: {self.req.get_url_stack()}")
|
||||
# print("==================")
|
||||
# print(self.req.get_resource_origin(0))
|
||||
# print(len(self.req.get_url_stack()))
|
||||
# print(self.resource._model_dump_excluded_)
|
||||
# print(type(self.resource))
|
||||
# print(self.resource.exclude)
|
||||
|
||||
if self.req.get_resource_origin(0) not in self.resource.model_fields:
|
||||
raise RestResourceHandlerException_ResourceNotFound(f"Unknown field access detected: {self.req.get_url_stack()}")
|
||||
|
||||
self.resource.check_acl_field(self.req)
|
||||
|
||||
if len(self.req.get_url_stack()) == 0: # destination reached
|
||||
if self.resource.model_fields[self.req.get_resource_origin(0)].exclude is True and self.req.get_verb() is rsrc_verb.GET:
|
||||
raise RestResourceHandlerException_ResourceNotFound(f"Not allowed READ access detected: {self.req.get_url_stack()}")
|
||||
|
||||
def _handle_process_get(self, params) -> RestResourceBase:
|
||||
# print(f"{type(self).__name__}->_process_get()")
|
||||
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
|
||||
|
||||
# CASE 1: no more item in url_stack => we reached the endpoint
|
||||
# CASE 1: no more item in url_stack => we reached the endpoint (operation)
|
||||
# So we are in a RestResourceBase instance and must return the content
|
||||
if len(self.req.get_url_stack()) == 0:
|
||||
self.resource.check_acl_self(self.req, None)
|
||||
for key, attr in self.resource.model_fields.items():
|
||||
if key in self.resource._plugins_:
|
||||
if isinstance(self.resource._plugins_[key], ResourcePlugin_field):
|
||||
plugin_field: ResourcePlugin_field = cast(ResourcePlugin_field, self.resource._plugins_[key])
|
||||
if issubclass(self.resource._plugins_[key], ResourcePlugin_field):
|
||||
plugin_field: ResourcePlugin_field = cast(
|
||||
ResourcePlugin_field, self.resource._plugins_[key](self.req, self.req.get_root_resource())
|
||||
)
|
||||
value = getattr(self.resource, key)
|
||||
setattr(self.resource, key, plugin_field.handle_field_get(value, params))
|
||||
elif isinstance(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
|
||||
plugin_field: ResourcePlugin_field = cast(ResourcePlugin_RestResourceBase, self.resource._plugins_[key])
|
||||
elif issubclass(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
|
||||
plugin_field: ResourcePlugin_field = cast(
|
||||
ResourcePlugin_RestResourceBase, self.resource._plugins_[key](self.req, self.req.get_root_resource())
|
||||
)
|
||||
value = getattr(self.resource, key)
|
||||
setattr(self.resource, key, plugin_field.handle_resource_get(value, params))
|
||||
|
||||
@@ -483,27 +500,28 @@ class ResourceHandler_RestResourceBase(
|
||||
# print(result)
|
||||
return self.resource
|
||||
|
||||
# CASE 2: specific case for root Node
|
||||
# CASE 2: specific (operation) case for root Node
|
||||
# TODO: this must probably be merged with the previous bloc
|
||||
if self.req.get_resource_origin(0) == "/":
|
||||
return self.resource
|
||||
|
||||
# CASE 3: in between
|
||||
# CASE 3: in between (access)
|
||||
self.resource.check_acl_field(self.req)
|
||||
value = getattr(self.resource, self.req.get_resource_origin(0))
|
||||
|
||||
key = self.req.get_resource_origin(0)
|
||||
if key in self.resource._plugins_:
|
||||
if isinstance(self.resource._plugins_[key], ResourcePlugin_field):
|
||||
if issubclass(self.resource._plugins_[key], ResourcePlugin_field):
|
||||
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
|
||||
ResourcePlugin_RestResourceBase,
|
||||
self.resource._plugins_[key],
|
||||
self.resource._plugins_[key](self.req, self.req.get_root_resource()),
|
||||
)
|
||||
value = plugin_rsrc.handle_field_get(value, params)
|
||||
|
||||
elif isinstance(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
|
||||
elif issubclass(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
|
||||
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
|
||||
ResourcePlugin_RestResourceBase,
|
||||
self.resource._plugins_[key],
|
||||
self.resource._plugins_[key](self.req, self.req.get_root_resource()),
|
||||
)
|
||||
value = plugin_rsrc.handle_resource_get(value, params)
|
||||
|
||||
@@ -513,6 +531,8 @@ class ResourceHandler_RestResourceBase(
|
||||
# print(f"{type(self).__name__}->_process_put()")
|
||||
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
|
||||
|
||||
self.resource.check_acl_self(self.req, self.req.get_data())
|
||||
|
||||
# creating a copy of the current resource
|
||||
_new_resrc = self.resource.copy()
|
||||
# updating values based on nex data
|
||||
@@ -522,16 +542,17 @@ class ResourceHandler_RestResourceBase(
|
||||
if isinstance(_new_resrc, RestResourceBase):
|
||||
for key, attr in _new_resrc.model_fields.items():
|
||||
if key in _new_resrc._plugins_:
|
||||
if isinstance(_new_resrc._plugins_[key], ResourcePlugin_field):
|
||||
plugin_field: ResourcePlugin_field = cast(ResourcePlugin_field, _new_resrc._plugins_[key])
|
||||
if issubclass(_new_resrc._plugins_[key], ResourcePlugin_field):
|
||||
plugin_field: ResourcePlugin_field = cast(
|
||||
ResourcePlugin_field, _new_resrc._plugins_[key](self.req, self.req.get_root_resource())
|
||||
)
|
||||
value = getattr(_new_resrc, key)
|
||||
setattr(_new_resrc, key, plugin_field.handle_field_put(value, params))
|
||||
|
||||
# applying plugins (from parent element)
|
||||
if self.prev_handler is not None:
|
||||
# element is within a dict
|
||||
if (
|
||||
isinstance(self.prev_handler.resource, dict)
|
||||
isinstance(self.prev_handler.resource, dict) # element is within a dict
|
||||
and self.prev_handler.prev_handler is not None
|
||||
and isinstance(self.prev_handler.prev_handler.resource, RestResourceBase)
|
||||
):
|
||||
@@ -539,7 +560,7 @@ class ResourceHandler_RestResourceBase(
|
||||
if key in self.prev_handler.prev_handler.resource._plugins_:
|
||||
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
|
||||
ResourcePlugin_RestResourceBase,
|
||||
self.prev_handler.prev_handler.resource._plugins_[key],
|
||||
self.prev_handler.prev_handler.resource._plugins_[key](self.req, self.req.get_root_resource()),
|
||||
)
|
||||
_new_resrc = plugin_rsrc.handle_dict_elem_put(_new_resrc, params)
|
||||
# element is within a RestResourceBase
|
||||
@@ -548,11 +569,11 @@ class ResourceHandler_RestResourceBase(
|
||||
if key in self.prev_handler.resource._plugins_:
|
||||
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
|
||||
ResourcePlugin_RestResourceBase,
|
||||
self.prev_handler.resource._plugins_[key],
|
||||
self.prev_handler.resource._plugins_[key](self.req, self.req.get_root_resource()),
|
||||
)
|
||||
_new_resrc = plugin_rsrc.handle_resource_put(_new_resrc, params)
|
||||
|
||||
self.resource.update(**_new_resrc.dict())
|
||||
self.resource.update(**_new_resrc.__dict__)
|
||||
return
|
||||
|
||||
def _handle_process_delete(self, params) -> None:
|
||||
@@ -568,7 +589,7 @@ class ResourceHandler_RestResourceBase(
|
||||
):
|
||||
self.prev_handler._process_delete()
|
||||
else:
|
||||
raise RuntimeError("cannot delete an element outside a dict")
|
||||
raise RestResourceHandlerException_BadRequest("cannot delete an element outside a dict")
|
||||
|
||||
|
||||
@ResourceHandler.register_resource_handler
|
||||
@@ -594,10 +615,12 @@ class ResourceHandler_simple(
|
||||
assert self.prev_handler is not None
|
||||
assert isinstance(self.prev_handler.resource, RestResourceBase)
|
||||
|
||||
self.prev_handler.resource.check_acl_field(self.req, 1)
|
||||
|
||||
if self.req.get_resource_origin(1) in self.prev_handler.resource._plugins_:
|
||||
plugin_simple: ResourcePlugin_field = cast(
|
||||
ResourcePlugin_field,
|
||||
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)],
|
||||
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)](self.req, self.req.get_root_resource()),
|
||||
)
|
||||
return plugin_simple.handle_field_get(self.resource, params)
|
||||
|
||||
@@ -610,22 +633,24 @@ class ResourceHandler_simple(
|
||||
assert self.prev_handler is not None
|
||||
assert isinstance(self.prev_handler.resource, RestResourceBase)
|
||||
|
||||
self.prev_handler.resource.check_acl_field(self.req, 1)
|
||||
|
||||
value = self.req.get_data()
|
||||
|
||||
if self.req.get_resource_origin(1) in self.prev_handler.resource._plugins_:
|
||||
print("PLUGIN FOUND")
|
||||
# print("PLUGIN FOUND")
|
||||
plugin_simple: ResourcePlugin_field = cast(
|
||||
ResourcePlugin_field,
|
||||
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)],
|
||||
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)](self.req, self.req.get_root_resource()),
|
||||
)
|
||||
print(value)
|
||||
# print(value)
|
||||
value = plugin_simple.handle_field_put(value, params)
|
||||
print(value)
|
||||
# print(value)
|
||||
|
||||
print(self.req.get_resource_origin(1))
|
||||
# print(self.req.get_resource_origin(1))
|
||||
setattr(
|
||||
self.prev_handler.resource,
|
||||
self.req.get_resource_origin(1),
|
||||
value,
|
||||
)
|
||||
print(self.prev_handler.resource)
|
||||
# print(self.prev_handler.resource)
|
||||
|
||||
@@ -12,14 +12,7 @@
|
||||
|
||||
"""CLI interface module"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
ClassVar,
|
||||
get_args,
|
||||
get_origin,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .rest_resource_walker import (
|
||||
RestResourceWalkerFutureResult,
|
||||
@@ -29,6 +22,9 @@ from .rest_resource_walker import (
|
||||
RestResourceWalker_Sub_RestResourceBase,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING is True:
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RestResourceWalkerFutureResult_RestResourceBase_handler(RestResourceWalkerFutureResult[dict]):
|
||||
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from __future__ import annotations
|
||||
from typing import Optional, Generic, TYPE_CHECKING
|
||||
|
||||
from typing import Optional, Protocol, runtime_checkable, TYPE_CHECKING
|
||||
from abc import abstractmethod
|
||||
from abc import abstractmethod, ABC
|
||||
from datetime import datetime
|
||||
|
||||
from .rest_types import (
|
||||
_T_DictValues,
|
||||
@@ -9,9 +10,11 @@ from .rest_types import (
|
||||
TV_SupportedRESTFields,
|
||||
TV_RestResourceBase,
|
||||
)
|
||||
from .rest_request import RestRequest
|
||||
|
||||
|
||||
if TYPE_CHECKING or True:
|
||||
if TYPE_CHECKING is True:
|
||||
from .rest_resource import RestResourceBase
|
||||
from .rest_request_opt import (
|
||||
RestRequestParams_GET,
|
||||
RestRequestParams_PUT,
|
||||
@@ -25,8 +28,31 @@ if TYPE_CHECKING or True:
|
||||
)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ResourcePlugin_field(Protocol[TV_SupportedRESTFields]):
|
||||
class ResourcePlugin(ABC):
|
||||
def __init__(self, request: RestRequest, root_resource: RestResourceBase) -> None:
|
||||
self.__request: RestRequest = request
|
||||
self.__root_resource: RestRequest = root_resource
|
||||
|
||||
def user_login(self, user_name: str, user_secret: str) -> str:
|
||||
return self.__root_resource.user_login(user_name, user_secret, self.__request)
|
||||
|
||||
def get_user_login(self) -> str:
|
||||
return self.__request.get_user().name
|
||||
|
||||
def set_resp_cookie_value(self, key: str, value: str) -> None:
|
||||
self.__request.set_resp_cookie_value(key, value)
|
||||
|
||||
def reset_resp_cookie(self, key: str) -> None:
|
||||
self.__request.reset_resp_cookie(key)
|
||||
|
||||
def get_new_cookie_expiration_date(self) -> datetime:
|
||||
return self.__root_resource.get_new_cookie_expiration_date()
|
||||
|
||||
def set_resp_status(self, status: int) -> None:
|
||||
self.__request.set_resp_status(status)
|
||||
|
||||
|
||||
class ResourcePlugin_field(ResourcePlugin, Generic[TV_SupportedRESTFields]):
|
||||
@abstractmethod
|
||||
def handle_field_get(self, resource: TV_SupportedRESTFields, params: RestRequestParams_GET) -> TV_SupportedRESTFields:
|
||||
...
|
||||
@@ -46,8 +72,7 @@ class ResourcePlugin_field_default(ResourcePlugin_field[TV_SupportedRESTFields])
|
||||
return resource
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ResourcePlugin_RestResourceBase(Protocol[TV_RestResourceBase]):
|
||||
class ResourcePlugin_RestResourceBase(ResourcePlugin, Generic[TV_RestResourceBase]):
|
||||
@abstractmethod
|
||||
def handle_resource_get(
|
||||
self,
|
||||
@@ -83,8 +108,7 @@ class ResourcePlugin_RestResourceBase_default(ResourcePlugin_RestResourceBase[TV
|
||||
return resource
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ResourcePlugin_dict(Protocol[_T_DictKey, _T_DictValues]):
|
||||
class ResourcePlugin_dict(ResourcePlugin, Generic[_T_DictKey, _T_DictValues]):
|
||||
@abstractmethod
|
||||
def handle_dict_get_keys(
|
||||
self,
|
||||
|
||||
171
src/pyrestresource/rest_resource_rootpoint.py
Normal file
171
src/pyrestresource/rest_resource_rootpoint.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
get_args,
|
||||
get_origin,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from .rest_resource import RestResourceBase
|
||||
from .rest_resource_plugin import (
|
||||
ResourcePlugin_field,
|
||||
ResourcePlugin_RestResourceBase,
|
||||
ResourcePlugin_dict,
|
||||
)
|
||||
from .rest_resource_walker import (
|
||||
RestResourceWalker_Root,
|
||||
RestResourceWalker_Sub_T_Dict,
|
||||
RestResourceWalker_Sub_RestFields,
|
||||
RestResourceWalker_Sub_RestResourceBase,
|
||||
)
|
||||
from .rest_types import rsrc_verb, _T_SupportedRESTFields
|
||||
from .rest_ACL import (
|
||||
ACL_record,
|
||||
ACL_target_group_Any,
|
||||
ACL_rule,
|
||||
)
|
||||
from .rest_exceptions import RestResourcePluginException_InvalidPluginSignature, RestResourceModelException, RestResourceModelException_ACL
|
||||
|
||||
if TYPE_CHECKING is True:
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_T_Dict__tree_init(RestResourceWalker_Sub_T_Dict):
|
||||
def process(self) -> None:
|
||||
datatype = get_args(self.annotation)
|
||||
|
||||
# checking compatibility
|
||||
if not get_origin(datatype[1]) is None:
|
||||
raise RestResourceModelException("complex dict types are not supported (should create a RestResourceBase container)")
|
||||
if not datatype[0] in _T_SupportedRESTFields:
|
||||
raise RestResourceModelException(f"Unsupported Dict Field value type in class (key)")
|
||||
|
||||
# preprocessing types / structure
|
||||
if self.parent is not None and isinstance(self.parent, RestResourceWalker_Sub_RestResourceBase):
|
||||
self.parent.annotation._dict_key_type_[self.resource_name] = datatype[0] # pylint: disable=protected-access
|
||||
self.parent.annotation._dict_value_type_[self.resource_name] = datatype[1] # pylint: disable=protected-access
|
||||
self.parent.annotation._model_dump_excluded_[self.resource_name] = True # pylint: disable=protected-access
|
||||
|
||||
self.resource.exclude = True
|
||||
self.parent.resource.model_rebuild(force=True)
|
||||
|
||||
self.parent.annotation._ACL_record_[self.resource_name] = []
|
||||
|
||||
if (
|
||||
isinstance(self.resource, FieldInfo)
|
||||
and self.resource.json_schema_extra is not None
|
||||
and type(self.resource.json_schema_extra) is dict
|
||||
):
|
||||
if "plugin" in self.resource.json_schema_extra:
|
||||
plugin_dict: ResourcePlugin_dict = self.resource.json_schema_extra["plugin"]
|
||||
if not issubclass(plugin_dict, ResourcePlugin_dict):
|
||||
raise RestResourcePluginException_InvalidPluginSignature()
|
||||
self.parent.annotation._plugins_[self.resource_name] = plugin_dict
|
||||
# print("ADD DICT PLUGIN")
|
||||
|
||||
if "ACL" in self.resource.json_schema_extra:
|
||||
if isinstance(self.resource.json_schema_extra["ACL"], list):
|
||||
# print(f"found ACL (Dict): {self.resource.json_schema_extra['ACL']}")
|
||||
self.parent.annotation._ACL_record_[self.resource_name] += self.resource.json_schema_extra["ACL"]
|
||||
else:
|
||||
raise RestResourceModelException_ACL("ACL must be a list()")
|
||||
|
||||
else:
|
||||
raise RestResourceModelException("dict must be contained in a RestResourceBase")
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_RestFields__tree_init(RestResourceWalker_Sub_RestFields):
|
||||
def process(self) -> None:
|
||||
if self.parent is not None and isinstance(self.parent, RestResourceWalker_Sub_RestResourceBase):
|
||||
# import pprint
|
||||
|
||||
# print("1aaaaaaaaaa")
|
||||
# pprint.pprint(self.resource.json_schema_extra)
|
||||
# pprint.pprint(self.annotation)
|
||||
# pprint.pprint(self.resource.exclude)
|
||||
|
||||
self.parent.annotation._ACL_record_[self.resource_name] = []
|
||||
|
||||
if (
|
||||
isinstance(self.resource, FieldInfo)
|
||||
and self.resource.json_schema_extra is not None
|
||||
and type(self.resource.json_schema_extra) is dict
|
||||
):
|
||||
# print("aaaaaaaaaa")
|
||||
|
||||
if "primary_key" in self.resource.json_schema_extra and self.resource.json_schema_extra["primary_key"] is True:
|
||||
if self.parent.annotation._primary_key_ is not None:
|
||||
raise RestResourceModelException(
|
||||
f"Only one primary key is allowed {self.parent.resource_name}.{self.resource_name}"
|
||||
)
|
||||
self.parent.annotation._primary_key_ = self.resource_name
|
||||
self.parent.annotation._ACL_record_[self.resource_name] = [
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY)
|
||||
]
|
||||
|
||||
if "plugin" in self.resource.json_schema_extra:
|
||||
plugin_field: ResourcePlugin_field = self.resource.json_schema_extra["plugin"]
|
||||
if not issubclass(plugin_field, ResourcePlugin_field):
|
||||
raise RestResourcePluginException_InvalidPluginSignature()
|
||||
self.parent.annotation._plugins_[self.resource_name] = plugin_field
|
||||
# print("ADD FIELD PLUGIN")
|
||||
|
||||
if "ACL" in self.resource.json_schema_extra:
|
||||
if isinstance(self.resource.json_schema_extra["ACL"], list):
|
||||
# print(f"found ACL (Field): {self.resource.json_schema_extra['ACL']}")
|
||||
self.parent.annotation._ACL_record_[self.resource_name] += self.resource.json_schema_extra["ACL"]
|
||||
else:
|
||||
raise RestResourceModelException_ACL("ACL must be a list()")
|
||||
|
||||
else:
|
||||
raise RestResourceModelException("fields must be contained in a RestResourceBase")
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_RestResourceBase__tree_init(RestResourceWalker_Sub_RestResourceBase):
|
||||
def process(self) -> None:
|
||||
setattr(self.annotation, "_dict_key_type_", {})
|
||||
setattr(self.annotation, "_dict_value_type_", {})
|
||||
setattr(self.annotation, "_model_dump_excluded_", {})
|
||||
setattr(self.annotation, "_primary_key_", None)
|
||||
setattr(self.annotation, "_plugins_", {})
|
||||
setattr(self.annotation, "_ACL_record_", {})
|
||||
|
||||
# preprocessing types / structure
|
||||
if self.parent is not None and isinstance(self.parent, RestResourceWalker_Sub_RestResourceBase):
|
||||
self.parent.annotation._model_dump_excluded_[self.resource_name] = True
|
||||
self.resource.exclude = True
|
||||
self.parent.resource.model_rebuild(force=True)
|
||||
self.parent.annotation._ACL_record_[self.resource_name] = []
|
||||
|
||||
if (
|
||||
isinstance(self.resource, FieldInfo)
|
||||
and self.resource.json_schema_extra is not None
|
||||
and type(self.resource.json_schema_extra) is dict
|
||||
):
|
||||
if "plugin" in self.resource.json_schema_extra:
|
||||
plugin_resource: ResourcePlugin_RestResourceBase = self.resource.json_schema_extra["plugin"]
|
||||
if not issubclass(plugin_resource, ResourcePlugin_RestResourceBase):
|
||||
raise RestResourcePluginException_InvalidPluginSignature()
|
||||
self.parent.annotation._plugins_[self.resource_name] = plugin_resource
|
||||
# print("ADD RESOURCE PLUGIN")
|
||||
|
||||
if "ACL" in self.resource.json_schema_extra:
|
||||
if isinstance(self.resource.json_schema_extra["ACL"], list):
|
||||
# print(f"found ACL (Resource): {self.resource.json_schema_extra['ACL']}")
|
||||
self.parent.annotation._ACL_record_[self.resource_name] += self.resource.json_schema_extra["ACL"]
|
||||
else:
|
||||
raise RestResourceModelException_ACL("ACL must be a list()")
|
||||
|
||||
|
||||
class RestResourceWalker_Root__tree_init(RestResourceWalker_Root):
|
||||
cls_RestResourceWalker_Sub = [
|
||||
RestResourceWalker_Sub_T_Dict__tree_init,
|
||||
RestResourceWalker_Sub_RestFields__tree_init,
|
||||
RestResourceWalker_Sub_RestResourceBase__tree_init,
|
||||
]
|
||||
|
||||
|
||||
def register_rest_rootpoint(klass: type[RestResourceBase]):
|
||||
RestResourceWalker_Root__tree_init(klass).process()
|
||||
return klass
|
||||
@@ -1,26 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
cast,
|
||||
Any,
|
||||
Optional,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
TypeVar,
|
||||
Type,
|
||||
Generic,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from typing import Type
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from .rest_types import _T_SupportedRESTFields
|
||||
from .rest_resource import RestResourceBase
|
||||
from .rest_exceptions import RestResourceModelException
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .rest_resource import RestResourceBase
|
||||
if TYPE_CHECKING is True:
|
||||
from typing import Any, Optional
|
||||
|
||||
TV_RestResourceWalkerFutureResult = TypeVar("TV_RestResourceWalkerFutureResult")
|
||||
|
||||
@@ -42,7 +40,7 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def check_type(cls, resource: FieldInfo | Type["RestResourceBase"]) -> tuple[bool, Type[Any], bool]:
|
||||
def check_type(cls, resource: FieldInfo | Type[RestResourceBase]) -> tuple[bool, Type[Any], bool]:
|
||||
"""implementation interface to Factory.
|
||||
The factory will call this specialized method on each implementation to find a supported one.
|
||||
"""
|
||||
@@ -53,27 +51,30 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
|
||||
self,
|
||||
subs: list[type[RestResourceWalker_Sub]],
|
||||
resource_name: str,
|
||||
resource: FieldInfo | Type["RestResourceBase"],
|
||||
resource: FieldInfo | Type[RestResourceBase],
|
||||
parent: Optional[RestResourceWalker_Sub] = None,
|
||||
argument: Optional[any] = None,
|
||||
) -> Optional[RestResourceWalker_Sub]:
|
||||
for sub in subs:
|
||||
_is_valid, _anno, _optional = sub.check_type(resource)
|
||||
|
||||
if _is_valid is True:
|
||||
return sub(resource_name, resource, parent, _anno, _optional)
|
||||
raise RuntimeError(f"Incompatible Field Found: {type(resource).__name__}")
|
||||
return sub(resource_name, resource, parent, _anno, _optional, argument)
|
||||
raise RestResourceModelException(f"Incompatible Field Found: {type(resource).__name__}")
|
||||
return None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
resource_name: str,
|
||||
resource: FieldInfo | Type["RestResourceBase"],
|
||||
resource: FieldInfo | Type[RestResourceBase],
|
||||
parent: Optional[RestResourceWalker_Sub] = None,
|
||||
annotation: Optional[type["RestResourceBase"]] = None,
|
||||
optional: Optional[bool] = None,
|
||||
annotation: Optional[type[RestResourceBase]] = None,
|
||||
_optional: Optional[bool] = None,
|
||||
argument: Optional[any] = None,
|
||||
):
|
||||
self.argument: any = argument
|
||||
self.resource_name: str = resource_name
|
||||
self.resource: FieldInfo | Type["RestResourceBase"] = resource
|
||||
self.resource: FieldInfo | Type[RestResourceBase] = resource
|
||||
self.parent: Optional[RestResourceWalker_Sub] = parent
|
||||
|
||||
self.future_results_subs: Optional[list[RestResourceWalkerFutureResult[TV_RestResourceWalkerFutureResult]]] = None
|
||||
@@ -82,48 +83,19 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
|
||||
self.future_results_subs = []
|
||||
self.future_result = self.cls_RestResourceWalkerFutureResult(self)
|
||||
|
||||
self.annotation: type["RestResourceBase"]
|
||||
self.annotation: type[RestResourceBase]
|
||||
self.optional: bool
|
||||
if annotation is None or optional is None:
|
||||
if annotation is None or _optional is None:
|
||||
self.annotation, self.optional = self.ProcessAnnotation(resource)
|
||||
else:
|
||||
self.annotation = annotation
|
||||
self.optional = optional
|
||||
self.optional = _optional
|
||||
|
||||
if self.annotation is None:
|
||||
raise RuntimeError("Only annotated types are allowed in RestResourceBase derived classes")
|
||||
raise RestResourceModelException("Only annotated types are allowed in RestResourceBase derived classes")
|
||||
|
||||
self.subdatatype = get_args(self.annotation)
|
||||
|
||||
# self.info()
|
||||
|
||||
def info(self) -> None:
|
||||
print(f"{type(self).__name__}->info()")
|
||||
print("==========================")
|
||||
print(f"resource_name: {self.resource_name}")
|
||||
print(f"resource: {type(self.resource).__name__}")
|
||||
print(f"resource: {self.resource}")
|
||||
print(f"parent: {self.parent}")
|
||||
print(f"annotation: {self.annotation}")
|
||||
print(f"optional: {self.optional}")
|
||||
print(f"subdatatype: {self.subdatatype}")
|
||||
|
||||
# -> cannot do that on dicts
|
||||
# if self.parent is not None:
|
||||
# print(f"_model_dump_excluded_: {self.parent.annotation._model_dump_excluded_}")
|
||||
|
||||
if False:
|
||||
print("------ STACK ------")
|
||||
_rsrc = self.parent
|
||||
while _rsrc is not None:
|
||||
print(f"{id(_rsrc.annotation)}:{_rsrc.annotation}")
|
||||
_rsrc = _rsrc.parent
|
||||
print("-------------------")
|
||||
|
||||
@classmethod
|
||||
def init_sub(cls, walker: RestResourceWalker_Root) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_future(self) -> Optional[RestResourceWalkerFutureResult]:
|
||||
return self.future_result
|
||||
@@ -152,9 +124,9 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
|
||||
|
||||
@staticmethod
|
||||
def ProcessAnnotation(
|
||||
resource: FieldInfo | Type["RestResourceBase"],
|
||||
resource: FieldInfo | Type[RestResourceBase],
|
||||
) -> tuple[type[Any], bool]:
|
||||
from .rest_resource import RestResourceBase
|
||||
# from .rest_resource import RestResourceBase
|
||||
|
||||
_anno: Type[Any]
|
||||
|
||||
@@ -167,7 +139,7 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
|
||||
elif not isinstance(resource, FieldInfo) and issubclass(resource, RestResourceBase):
|
||||
_anno = resource
|
||||
else:
|
||||
raise RuntimeError("Incompatible resource type")
|
||||
raise RestResourceModelException("Incompatible resource type")
|
||||
|
||||
_datatype = get_args(_anno)
|
||||
_optional: bool = False
|
||||
@@ -180,19 +152,21 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
|
||||
_anno = _datatype[0]
|
||||
_optional = True
|
||||
else:
|
||||
raise RuntimeError("Union is only allowed to describe Optional (e.g. Union[XXX,None])")
|
||||
raise RestResourceModelException("Union is only allowed to describe Optional (e.g. Union[XXX,None])")
|
||||
|
||||
return _anno, _optional
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_T_Dict(RestResourceWalker_Sub):
|
||||
@classmethod
|
||||
def check_type(cls, resource: FieldInfo | Type["RestResourceBase"]) -> tuple[bool, Type[Any], bool]:
|
||||
def check_type(cls, resource: FieldInfo | Type[RestResourceBase]) -> tuple[bool, Type[Any], bool]:
|
||||
_anno, _optional = cls.ProcessAnnotation(resource)
|
||||
_type_resource = get_origin(_anno)
|
||||
return (_type_resource is dict), _anno, _optional
|
||||
|
||||
def get_sub_resources(self) -> list[tuple[str, FieldInfo]]:
|
||||
# print("????")
|
||||
# print(self.subdatatype[1])
|
||||
return [(self.resource_name, self.subdatatype[1])]
|
||||
|
||||
def get_future(self) -> Optional[RestResourceWalkerFutureResult]:
|
||||
@@ -201,7 +175,7 @@ class RestResourceWalker_Sub_T_Dict(RestResourceWalker_Sub):
|
||||
|
||||
class RestResourceWalker_Sub_RestFields(RestResourceWalker_Sub):
|
||||
@classmethod
|
||||
def check_type(cls, resource: FieldInfo | Type["RestResourceBase"]) -> tuple[bool, Type[Any], bool]:
|
||||
def check_type(cls, resource: FieldInfo | Type[RestResourceBase]) -> tuple[bool, Type[Any], bool]:
|
||||
_anno, _optional = cls.ProcessAnnotation(resource)
|
||||
return (_anno in _T_SupportedRESTFields), _anno, _optional
|
||||
|
||||
@@ -211,9 +185,7 @@ class RestResourceWalker_Sub_RestFields(RestResourceWalker_Sub):
|
||||
|
||||
class RestResourceWalker_Sub_RestResourceBase(RestResourceWalker_Sub):
|
||||
@classmethod
|
||||
def check_type(cls, resource: FieldInfo | Type["RestResourceBase"]) -> tuple[bool, Type[Any], bool]:
|
||||
from .rest_resource import RestResourceBase
|
||||
|
||||
def check_type(cls, resource: FieldInfo | Type[RestResourceBase]) -> tuple[bool, Type[Any], bool]:
|
||||
_anno, _optional = cls.ProcessAnnotation(resource)
|
||||
return (
|
||||
((get_origin(_anno) is None) and issubclass(_anno, RestResourceBase)),
|
||||
@@ -235,28 +207,25 @@ class RestResourceWalker_Root:
|
||||
RestResourceWalker_Sub_RestResourceBase,
|
||||
]
|
||||
|
||||
def __init__(self, resource: "RestResourceBase" | Type["RestResourceBase"]) -> None:
|
||||
from .rest_resource import RestResourceBase
|
||||
|
||||
self.resource: Type["RestResourceBase"]
|
||||
def __init__(self, resource: RestResourceBase | Type[RestResourceBase]) -> None:
|
||||
self.subwalker_argument: any = None
|
||||
self.resource: Type[RestResourceBase]
|
||||
if isinstance(resource, RestResourceBase):
|
||||
self.resource = type(resource)
|
||||
else:
|
||||
self.resource = resource
|
||||
|
||||
def process(self, deep_limit: Optional[int] = None) -> Optional[TV_RestResourceWalkerFutureResult]:
|
||||
def process(self, argument: Optional[any] = None, deep_limit: Optional[int] = None) -> Optional[TV_RestResourceWalkerFutureResult]:
|
||||
current_deep: int = 0
|
||||
for cls_Sub in self.cls_RestResourceWalker_Sub:
|
||||
_self = self
|
||||
cls_Sub.init_sub(_self)
|
||||
|
||||
sub_walker_initial: Optional[RestResourceWalker_Sub] = RestResourceWalker_Sub.get(
|
||||
self.cls_RestResourceWalker_Sub, "/", self.resource, None
|
||||
self.cls_RestResourceWalker_Sub, "/", self.resource, None, argument
|
||||
)
|
||||
|
||||
if sub_walker_initial is not None:
|
||||
sub_walker_initial.process()
|
||||
sub_walker_initial.get_future()
|
||||
resource_list: list[tuple[str, FieldInfo | Type["RestResourceBase"], RestResourceWalker_Sub]] = [
|
||||
resource_list: list[tuple[str, FieldInfo | Type[RestResourceBase], RestResourceWalker_Sub]] = [
|
||||
(subresource_name, subresource, sub_walker_initial)
|
||||
for subresource_name, subresource in sub_walker_initial.get_sub_resources()
|
||||
]
|
||||
@@ -267,10 +236,7 @@ class RestResourceWalker_Root:
|
||||
new_resource_list = []
|
||||
for resource_name, resource, parent_sub_walker in resource_list:
|
||||
sub_walker = RestResourceWalker_Sub.get(
|
||||
self.cls_RestResourceWalker_Sub,
|
||||
resource_name,
|
||||
resource,
|
||||
parent_sub_walker,
|
||||
self.cls_RestResourceWalker_Sub, resource_name, resource, parent_sub_walker, argument
|
||||
)
|
||||
if sub_walker is not None:
|
||||
sub_walker.process()
|
||||
@@ -287,5 +253,5 @@ class RestResourceWalker_Root:
|
||||
current_deep = current_deep + 1
|
||||
return sub_walker_initial.chain_process_future()
|
||||
else:
|
||||
raise RuntimeError("Invalid Rootpoint")
|
||||
raise RestResourceModelException("Invalid Rootpoint")
|
||||
return None
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
|
||||
from __future__ import annotations
|
||||
from enum import Enum, auto
|
||||
from typing import Union, get_origin, NewType, TypeVar, TYPE_CHECKING
|
||||
|
||||
from enum import Enum, auto
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
from ipaddress import IPv4Address, IPv4Network
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .rest_resource import RestResourceBase
|
||||
if TYPE_CHECKING is True:
|
||||
pass
|
||||
|
||||
|
||||
T_Gen_DictKeys: type = type({}.keys())
|
||||
NoneType = type(None)
|
||||
@@ -39,10 +41,9 @@ _T_SupportedRESTFields = [
|
||||
Path,
|
||||
IPv4Address,
|
||||
IPv4Network,
|
||||
NoneType,
|
||||
]
|
||||
T_SupportedRESTFields = Union[
|
||||
UUID, str, int, float, bool, bytes, datetime, Path, IPv4Address, IPv4Network
|
||||
]
|
||||
T_SupportedRESTFields = Union[UUID, str, int, float, bool, bytes, datetime, Path, IPv4Address, IPv4Network, NoneType]
|
||||
TV_SupportedRESTFields = TypeVar(
|
||||
"TV_SupportedRESTFields",
|
||||
UUID,
|
||||
@@ -55,10 +56,10 @@ TV_SupportedRESTFields = TypeVar(
|
||||
Path,
|
||||
IPv4Address,
|
||||
IPv4Network,
|
||||
NoneType,
|
||||
)
|
||||
|
||||
if get_origin(T_SupportedRESTFields) is not Union:
|
||||
raise RuntimeError("wrong T_SupportedRESTFields (must be flat Union)")
|
||||
assert get_origin(T_SupportedRESTFields) is Union
|
||||
|
||||
TV_RestResourceBase = TypeVar("TV_RestResourceBase", bound="RestResourceBase")
|
||||
|
||||
@@ -68,12 +69,8 @@ T_FieldValue = Union[T_SupportedRESTFields, "RestResourceBase"]
|
||||
T_ListIndex = NewType("T_ListIndex", int)
|
||||
T_ListSize = NewType("T_ListSize", int)
|
||||
|
||||
T_DictKey = Union[
|
||||
UUID, str, int, float, bool, bytes, Path, IPv4Address, IPv4Network
|
||||
] # datetime is removed because non-hashable
|
||||
_T_DictKey = TypeVar(
|
||||
"_T_DictKey", UUID, str, int, float, bool, bytes, Path, IPv4Address, IPv4Network
|
||||
)
|
||||
T_DictKey = Union[UUID, str, int, float, bool, bytes, Path, IPv4Address, IPv4Network] # datetime is removed because non-hashable
|
||||
_T_DictKey = TypeVar("_T_DictKey", UUID, str, int, float, bool, bytes, Path, IPv4Address, IPv4Network)
|
||||
|
||||
T_T_DictKey = type[T_DictKey]
|
||||
|
||||
@@ -92,6 +89,7 @@ _T_DictValues = TypeVar(
|
||||
IPv4Address,
|
||||
IPv4Network,
|
||||
"RestResourceBase",
|
||||
NoneType,
|
||||
)
|
||||
|
||||
T_T_FieldValue = type(T_FieldValue)
|
||||
@@ -101,5 +99,4 @@ T_Dict = dict[T_DictKey, T_DictValues]
|
||||
_T_Dict = dict[_T_DictKey, _T_DictValues]
|
||||
|
||||
T_AllSupportedFields = T_Dict | T_FieldValue
|
||||
T_AllSupportedFiels = T_Dict | T_FieldValue
|
||||
T_AllSupportedContainers = Union[T_Dict, "RestResourceBase"]
|
||||
|
||||
178
test/test_ACL.py
Normal file
178
test/test_ACL.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
import unittest
|
||||
from os import chdir
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from pydantic import Field
|
||||
|
||||
from src.pyrestresource import (
|
||||
RestResourceHandlerException_Forbiden,
|
||||
register_rest_rootpoint,
|
||||
RestResourceBase,
|
||||
rsrc_verb,
|
||||
RestRequestParams_GET,
|
||||
RestRequestParams_POST,
|
||||
RestRequestParams_Dict_GET,
|
||||
RestRequestParams_PUT,
|
||||
T_SupportedRESTFields,
|
||||
ResourcePlugin_field_default,
|
||||
ResourcePlugin_RestResourceBase_default,
|
||||
ACL_target_group_Any,
|
||||
ACL_record,
|
||||
ACL_rule,
|
||||
)
|
||||
|
||||
|
||||
testdir_path = Path(__file__).parent.resolve()
|
||||
chdir(testdir_path.parent.resolve())
|
||||
|
||||
|
||||
# to allow mock-ing, all the tested classes are in a function
|
||||
def init_classes():
|
||||
class TestResource(RestResourceBase):
|
||||
username: Optional[str] = Field(None)
|
||||
secret: Optional[str] = Field(
|
||||
None,
|
||||
exclude=True,
|
||||
ACL=[
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.ALLOW),
|
||||
ACL_record(verbs=[rsrc_verb.GET], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
|
||||
],
|
||||
)
|
||||
|
||||
class TestResource2(RestResourceBase):
|
||||
version_ro: Optional[str] = Field(
|
||||
"1.2.3",
|
||||
ACL=[
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
|
||||
],
|
||||
)
|
||||
version: Optional[str] = Field("3.2.1")
|
||||
|
||||
@register_rest_rootpoint
|
||||
class RootApp(RestResourceBase):
|
||||
resource_with_secret: TestResource = Field(default=TestResource())
|
||||
resource_with_secret_ACL: TestResource = Field(
|
||||
default=TestResource(), ACL=[ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY)]
|
||||
)
|
||||
resource_ro: TestResource2 = Field(TestResource2())
|
||||
|
||||
# this add the classes to globals to allow using them later on
|
||||
# => this is only for uinit-testing purpose and is not needed in real use
|
||||
globals()[TestResource.__name__] = TestResource
|
||||
globals()[RootApp.__name__] = RootApp
|
||||
|
||||
|
||||
class Test_RestAPI_ACL(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
chdir(testdir_path.parent.resolve())
|
||||
init_classes()
|
||||
self.testapp = RootApp()
|
||||
|
||||
def test_subresource_readonly(self):
|
||||
result = self.testapp.process_request("/", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), "{}")
|
||||
|
||||
result = self.testapp.process_request("/resource_ro", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), '{"version_ro": "1.2.3", "version": "3.2.1"}')
|
||||
|
||||
self.testapp.process_request("/resource_ro/version", rsrc_verb.PUT, '"6.6.6"')
|
||||
|
||||
result = self.testapp.process_request("/resource_ro", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), '{"version_ro": "1.2.3", "version": "6.6.6"}')
|
||||
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request("/resource_ro/version_ro", rsrc_verb.PUT, '"6.6.6"')
|
||||
self.assertEqual(self.testapp.resource_ro.version_ro, "1.2.3")
|
||||
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request("/resource_ro", rsrc_verb.PUT, '{"version_ro": "6.6.1", "version": "6.6.2"}')
|
||||
self.assertEqual(self.testapp.resource_ro.version_ro, "1.2.3")
|
||||
|
||||
result = self.testapp.process_request("/resource_ro", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), '{"version_ro": "1.2.3", "version": "6.6.6"}')
|
||||
|
||||
def test_subresource(self):
|
||||
result = self.testapp.process_request("/", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), "{}")
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), '{"username": null}')
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret/username", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), "null")
|
||||
self.assertEqual(self.testapp.resource_with_secret.username, None)
|
||||
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden):
|
||||
self.testapp.process_request("/resource_with_secret/secret", rsrc_verb.GET)
|
||||
|
||||
self.assertEqual(self.testapp.resource_with_secret.secret, None)
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret", rsrc_verb.PUT, '{"username":"chacha","secret":"123456"}')
|
||||
self.assertEqual(result.get_result(), "null")
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), '{"username": "chacha"}')
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret/username", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), '"chacha"')
|
||||
self.assertEqual(self.testapp.resource_with_secret.username, "chacha")
|
||||
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden):
|
||||
self.testapp.process_request("/resource_with_secret/secret", rsrc_verb.GET)
|
||||
|
||||
self.assertEqual(self.testapp.resource_with_secret.secret, "123456")
|
||||
|
||||
def test_subresource_field(self):
|
||||
result = self.testapp.process_request("/resource_with_secret/username", rsrc_verb.PUT, '"chacha"')
|
||||
self.assertEqual(result.get_result(), "null")
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), '{"username": "chacha"}')
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret/username", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), '"chacha"')
|
||||
self.assertEqual(self.testapp.resource_with_secret.username, "chacha")
|
||||
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request("/resource_with_secret/secret", rsrc_verb.GET)
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret/secret", rsrc_verb.PUT, '"123456"')
|
||||
self.assertEqual(result.get_result(), "null")
|
||||
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request("/resource_with_secret/secret", rsrc_verb.GET)
|
||||
|
||||
self.assertEqual(self.testapp.resource_with_secret.secret, "123456")
|
||||
|
||||
def test_subresource_ACL(self):
|
||||
result = self.testapp.process_request("/", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), "{}")
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret_ACL", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), '{"username": null}')
|
||||
|
||||
result = self.testapp.process_request("/resource_with_secret_ACL/username", rsrc_verb.GET)
|
||||
self.assertEqual(result.get_result(), "null")
|
||||
self.assertEqual(self.testapp.resource_with_secret_ACL.username, None)
|
||||
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request("/resource_with_secret_ACL/secret", rsrc_verb.GET)
|
||||
|
||||
self.assertEqual(self.testapp.resource_with_secret_ACL.secret, None)
|
||||
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request("/resource_with_secret_ACL", rsrc_verb.PUT, '{"username":"chacha","secret":"123456"}')
|
||||
self.assertEqual(self.testapp.resource_with_secret_ACL.username, None)
|
||||
self.assertEqual(self.testapp.resource_with_secret_ACL.secret, None)
|
||||
|
||||
def test_subresource_ACL_field(self):
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request("/resource_with_secret_ACL/username", rsrc_verb.PUT, '"chacha"')
|
||||
self.assertEqual(self.testapp.resource_with_secret_ACL.username, None)
|
||||
self.assertEqual(self.testapp.resource_with_secret_ACL.secret, None)
|
||||
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request("/resource_with_secret_ACL/secret", rsrc_verb.PUT, '"123456"')
|
||||
self.assertEqual(self.testapp.resource_with_secret_ACL.username, None)
|
||||
self.assertEqual(self.testapp.resource_with_secret_ACL.secret, None)
|
||||
@@ -1,20 +1,23 @@
|
||||
from __future__ import annotations
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from os import chdir
|
||||
from pathlib import Path
|
||||
from typing import Optional, Annotated
|
||||
from typing import Optional, ClassVar
|
||||
from pydantic import Field
|
||||
from uuid import UUID, uuid4
|
||||
from time import time
|
||||
import json
|
||||
|
||||
print(__name__)
|
||||
print(__package__)
|
||||
from time import sleep
|
||||
import uvicorn
|
||||
import socket
|
||||
import requests
|
||||
from contextlib import closing
|
||||
from multiprocessing import Process
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from src.pyrestresource import (
|
||||
register_rest_rootpoint,
|
||||
ACL_target_user,
|
||||
UserLogin,
|
||||
RestResourceBase,
|
||||
RestResourceBaseLogin,
|
||||
register_rest_rootpoint,
|
||||
rsrc_verb,
|
||||
RestRequestParams_GET,
|
||||
RestRequestParams_POST,
|
||||
@@ -23,8 +26,11 @@ from src.pyrestresource import (
|
||||
T_SupportedRESTFields,
|
||||
ResourcePlugin_field_default,
|
||||
ResourcePlugin_RestResourceBase_default,
|
||||
ACL_target_group_Any,
|
||||
ACL_record,
|
||||
ACL_rule,
|
||||
)
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
testdir_path = Path(__file__).parent.resolve()
|
||||
chdir(testdir_path.parent.resolve())
|
||||
@@ -32,49 +38,614 @@ chdir(testdir_path.parent.resolve())
|
||||
|
||||
# to allow mock-ing, all the tested classes are in a function
|
||||
def init_classes():
|
||||
class ResourcePlugin_Login(ResourcePlugin_RestResourceBase_default):
|
||||
def handle_resource_get(self, resource: Login, params: RestRequestParams_GET) -> Login:
|
||||
print("hook GET")
|
||||
print(resource)
|
||||
print(params)
|
||||
return resource
|
||||
user_test = UserLogin(username="TestUser", secret="123456")
|
||||
user_test2 = UserLogin(username="TestUser2", secret="abcdef")
|
||||
|
||||
def handle_resource_put(self, resource: Login, params: RestRequestParams_GET) -> Login:
|
||||
print("hook PUT")
|
||||
print(resource)
|
||||
print(params)
|
||||
return resource
|
||||
class TestResource(RestResourceBase):
|
||||
test_field: Optional[str] = Field("ORIGIN_VALUE")
|
||||
|
||||
class Login(RestResourceBase):
|
||||
username: Optional[str] = Field(None, exclude=True)
|
||||
# username: Optional[str] = Field(None)
|
||||
secret: Optional[str] = Field(None, exclude=True)
|
||||
class TestResourceACL(RestResourceBase):
|
||||
test_field: Optional[str] = Field(
|
||||
"ORIGIN_VALUE",
|
||||
ACL=[
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_user.from_user_login(user_test), rule=ACL_rule.ALLOW),
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
|
||||
],
|
||||
)
|
||||
test_field2: Optional[str] = Field(
|
||||
"ORIGIN_VALUE",
|
||||
ACL=[
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_user.from_user_login(user_test2), rule=ACL_rule.ALLOW),
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
|
||||
],
|
||||
)
|
||||
test_field_both: Optional[str] = Field(
|
||||
"ORIGIN_VALUE",
|
||||
ACL=[
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_user.from_user_login(user_test), rule=ACL_rule.ALLOW),
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_user.from_user_login(user_test2), rule=ACL_rule.ALLOW),
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
|
||||
],
|
||||
)
|
||||
|
||||
@register_rest_rootpoint
|
||||
class RootApp(RestResourceBase):
|
||||
login: Login = Field(
|
||||
default=Login(),
|
||||
plugin=ResourcePlugin_Login,
|
||||
class RootApp(RestResourceBaseLogin):
|
||||
_ar_user_login: ClassVar[list[UserLogin]] = [user_test, user_test2]
|
||||
test_resourceACL: TestResource = Field(
|
||||
TestResource(),
|
||||
ACL=[
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_user(name=user_test.username), rule=ACL_rule.ALLOW),
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
|
||||
],
|
||||
)
|
||||
test_resource: TestResourceACL = TestResourceACL()
|
||||
|
||||
# this add the classes to globals to allow using them later on
|
||||
# => this is only for uinit-testing purpose and is not needed in real use
|
||||
globals()[Login.__name__] = Login
|
||||
globals()[TestResourceACL.__name__] = TestResourceACL
|
||||
globals()[RootApp.__name__] = RootApp
|
||||
|
||||
|
||||
class Test_RestAPI_LOGIN(unittest.TestCase):
|
||||
def find_free_port():
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||||
s.bind(("", 0))
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
return "localhost", s.getsockname()[1]
|
||||
|
||||
|
||||
def launch_server(ip, port):
|
||||
init_classes()
|
||||
uvicorn.run(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True)
|
||||
|
||||
|
||||
class Test_RestAPI_LOGIN_Web(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
chdir(testdir_path.parent.resolve())
|
||||
init_classes()
|
||||
self.testapp = RootApp()
|
||||
|
||||
def test_login_two_users(self):
|
||||
ip, port = find_free_port()
|
||||
proc = Process(
|
||||
target=launch_server,
|
||||
args=(
|
||||
ip,
|
||||
port,
|
||||
),
|
||||
)
|
||||
proc.start()
|
||||
sleep(1)
|
||||
s = requests.Session()
|
||||
s.mount("http://", HTTPAdapter(max_retries=0))
|
||||
|
||||
try:
|
||||
# login
|
||||
response = s.put(
|
||||
f"http://{ip}:{port}/login",
|
||||
json={"username": "TestUser", "secret": "123456"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# authenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource/test_field", json="TEST SET VALUE")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TEST SET VALUE")
|
||||
|
||||
# unauthenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource/test_field2", json="TEST SET VALUE")
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# not modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field2",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "ORIGIN_VALUE")
|
||||
|
||||
# authenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource/test_field_both", json="TEST SET VALUE 2")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field_both",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TEST SET VALUE 2")
|
||||
|
||||
# ---------------------------------------
|
||||
# login 2
|
||||
response = s.put(
|
||||
f"http://{ip}:{port}/login",
|
||||
json={"username": "TestUser2", "secret": "abcdef"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# unauthenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource/test_field", json="A TEST SET VALUE")
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# not modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TEST SET VALUE")
|
||||
|
||||
# authenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource/test_field2", json="A TEST SET VALUE")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field2",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "A TEST SET VALUE")
|
||||
|
||||
# previous (modified) value
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field_both",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TEST SET VALUE 2")
|
||||
|
||||
# authenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource/test_field_both", json="A TEST SET VALUE 2")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field_both",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "A TEST SET VALUE 2")
|
||||
|
||||
finally:
|
||||
proc.terminate()
|
||||
s.close()
|
||||
|
||||
def test_login(self):
|
||||
result = self.testapp.process_request("/login", rsrc_verb.GET)
|
||||
print(result)
|
||||
ip, port = find_free_port()
|
||||
proc = Process(
|
||||
target=launch_server,
|
||||
args=(
|
||||
ip,
|
||||
port,
|
||||
),
|
||||
)
|
||||
proc.start()
|
||||
sleep(1)
|
||||
s = requests.Session()
|
||||
s.mount("http://", HTTPAdapter(max_retries=0))
|
||||
|
||||
result = self.testapp.process_request("/login", rsrc_verb.PUT, '{"username":"toto","secret":"123456"}')
|
||||
print(result)
|
||||
try:
|
||||
# read full login resource
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "__ANNONYMOUS__"})
|
||||
|
||||
result = self.testapp.process_request("/login", rsrc_verb.GET)
|
||||
print(result)
|
||||
# read login username field
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "__ANNONYMOUS__")
|
||||
|
||||
# login
|
||||
response = s.put(
|
||||
f"http://{ip}:{port}/login",
|
||||
json={"username": "TestUser", "secret": "123456"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# read full login resource
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "TestUser"})
|
||||
|
||||
# read login username field
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TestUser")
|
||||
|
||||
finally:
|
||||
proc.terminate()
|
||||
s.close()
|
||||
|
||||
def test_change_host(self):
|
||||
ip, port = find_free_port()
|
||||
proc = Process(
|
||||
target=launch_server,
|
||||
args=(
|
||||
ip,
|
||||
port,
|
||||
),
|
||||
)
|
||||
proc.start()
|
||||
sleep(1)
|
||||
s1 = requests.Session()
|
||||
s1.mount("http://", HTTPAdapter(max_retries=0))
|
||||
s2 = requests.Session()
|
||||
s2.mount("http://", HTTPAdapter(max_retries=0))
|
||||
|
||||
try:
|
||||
# s1 - read full login resource
|
||||
response = s1.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "__ANNONYMOUS__"})
|
||||
|
||||
# s1 - read login username field
|
||||
response = s1.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "__ANNONYMOUS__")
|
||||
|
||||
# s2 - read full login resource
|
||||
response = s2.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "__ANNONYMOUS__"})
|
||||
|
||||
# s2 - read login username field
|
||||
response = s2.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "__ANNONYMOUS__")
|
||||
|
||||
# login s1
|
||||
response = s1.put(
|
||||
f"http://{ip}:{port}/login",
|
||||
json={"username": "TestUser", "secret": "123456"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# s1 - read full login resource
|
||||
response = s1.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "TestUser"})
|
||||
|
||||
# s1 - read login username field
|
||||
response = s1.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TestUser")
|
||||
|
||||
# s2 - read full login resource
|
||||
response = s2.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "__ANNONYMOUS__"})
|
||||
|
||||
# s2 - read login username field
|
||||
response = s2.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "__ANNONYMOUS__")
|
||||
|
||||
# s2 -> spoof s1 token
|
||||
s2.cookies.update(s1.cookies)
|
||||
|
||||
# s2 - read full login resource
|
||||
response = s2.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertDictEqual(s2.cookies.get_dict(), {})
|
||||
|
||||
# s2 - read full login resource (reseted cookie)
|
||||
response = s2.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "__ANNONYMOUS__"})
|
||||
|
||||
# s2 -> spoof s1 token
|
||||
s2.cookies.update(s1.cookies)
|
||||
|
||||
# s2 - read login username field
|
||||
response = s2.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertDictEqual(s2.cookies.get_dict(), {})
|
||||
|
||||
# s2 - read full login resource (reseted cookie)
|
||||
response = s2.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "__ANNONYMOUS__")
|
||||
|
||||
finally:
|
||||
proc.terminate()
|
||||
s1.close()
|
||||
s2.close()
|
||||
|
||||
def test_login_wrong_pwd(self):
|
||||
ip, port = find_free_port()
|
||||
proc = Process(
|
||||
target=launch_server,
|
||||
args=(
|
||||
ip,
|
||||
port,
|
||||
),
|
||||
)
|
||||
proc.start()
|
||||
sleep(1)
|
||||
s = requests.Session()
|
||||
s.mount("http://", HTTPAdapter(max_retries=0))
|
||||
|
||||
try:
|
||||
# read full login resource
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "__ANNONYMOUS__"})
|
||||
self.assertDictEqual(s.cookies.get_dict(), {})
|
||||
|
||||
# read login username field
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "__ANNONYMOUS__")
|
||||
self.assertDictEqual(s.cookies.get_dict(), {})
|
||||
|
||||
# ---------------------------------------------------
|
||||
# login (wrong pwd)
|
||||
response = s.put(
|
||||
f"http://{ip}:{port}/login",
|
||||
json={"username": "TestUser", "secret": "abc"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertDictEqual(s.cookies.get_dict(), {})
|
||||
|
||||
# read full login resource
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "__ANNONYMOUS__"})
|
||||
self.assertDictEqual(s.cookies.get_dict(), {})
|
||||
|
||||
# read login username field
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "__ANNONYMOUS__")
|
||||
self.assertDictEqual(s.cookies.get_dict(), {})
|
||||
|
||||
# ---------------------------------------------------
|
||||
# login (ok pwd)
|
||||
response = s.put(
|
||||
f"http://{ip}:{port}/login",
|
||||
json={"username": "TestUser", "secret": "123456"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue("Authorization" in response.cookies)
|
||||
self.assertTrue("Authorization" in s.cookies.get_dict())
|
||||
self.assertTrue(s.cookies.get_dict()["Authorization"])
|
||||
|
||||
# read full login resource
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "TestUser"})
|
||||
self.assertTrue("Authorization" in s.cookies.get_dict())
|
||||
self.assertTrue(s.cookies.get_dict()["Authorization"])
|
||||
|
||||
# read login username field
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TestUser")
|
||||
self.assertTrue("Authorization" in s.cookies.get_dict())
|
||||
self.assertTrue(s.cookies.get_dict()["Authorization"])
|
||||
|
||||
# ---------------------------------------------------
|
||||
# login (wrong pwd, after success)
|
||||
response = s.put(
|
||||
f"http://{ip}:{port}/login",
|
||||
json={"username": "TestUser", "secret": "abc"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertDictEqual(s.cookies.get_dict(), {})
|
||||
|
||||
# read full login resource
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.json(), {"username": "__ANNONYMOUS__"})
|
||||
self.assertDictEqual(s.cookies.get_dict(), {})
|
||||
|
||||
# read login username field
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/login/username",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "__ANNONYMOUS__")
|
||||
self.assertDictEqual(s.cookies.get_dict(), {})
|
||||
|
||||
finally:
|
||||
proc.terminate()
|
||||
s.close()
|
||||
|
||||
def test_access_resourceACL(self):
|
||||
ip, port = find_free_port()
|
||||
proc = Process(
|
||||
target=launch_server,
|
||||
args=(
|
||||
ip,
|
||||
port,
|
||||
),
|
||||
)
|
||||
proc.start()
|
||||
sleep(1)
|
||||
s = requests.Session()
|
||||
s.mount("http://", HTTPAdapter(max_retries=0))
|
||||
|
||||
try:
|
||||
# before modification read
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resourceACL/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "ORIGIN_VALUE")
|
||||
|
||||
# try unauthenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resourceACL/test_field", json="TEST SET VALUE")
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# check not modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resourceACL/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "ORIGIN_VALUE")
|
||||
|
||||
# try unauthenticated write (to resource)
|
||||
response = s.put(f"http://{ip}:{port}/test_resourceACL", json={"test_field": "TEST SET VALUE"})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# check not modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resourceACL/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "ORIGIN_VALUE")
|
||||
|
||||
# login
|
||||
response = s.put(
|
||||
f"http://{ip}:{port}/login",
|
||||
json={"username": "TestUser", "secret": "123456"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# authenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resourceACL/test_field", json="TEST SET VALUE")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resourceACL/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TEST SET VALUE")
|
||||
|
||||
# authenticated write (to resource)
|
||||
response = s.put(f"http://{ip}:{port}/test_resourceACL", json={"test_field": "TEST SET VALUE 2"})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resourceACL/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TEST SET VALUE 2")
|
||||
|
||||
finally:
|
||||
proc.terminate()
|
||||
s.close()
|
||||
|
||||
def test_access_fieldACL(self):
|
||||
ip, port = find_free_port()
|
||||
proc = Process(
|
||||
target=launch_server,
|
||||
args=(
|
||||
ip,
|
||||
port,
|
||||
),
|
||||
)
|
||||
proc.start()
|
||||
sleep(1)
|
||||
s = requests.Session()
|
||||
s.mount("http://", HTTPAdapter(max_retries=0))
|
||||
|
||||
try:
|
||||
# before modification read
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "ORIGIN_VALUE")
|
||||
|
||||
# try unauthenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource/test_field", json="TEST SET VALUE")
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# check not modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "ORIGIN_VALUE")
|
||||
|
||||
# try unauthenticated write (to resource)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource", json={"test_field": "TEST SET VALUE"})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# check not modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "ORIGIN_VALUE")
|
||||
|
||||
# login
|
||||
response = s.put(
|
||||
f"http://{ip}:{port}/login",
|
||||
json={"username": "TestUser", "secret": "123456"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# authenticated write (to field)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource/test_field", json="TEST SET VALUE")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TEST SET VALUE")
|
||||
|
||||
# authenticated write (to resource)
|
||||
response = s.put(f"http://{ip}:{port}/test_resource", json={"test_field": "TEST SET VALUE 2"})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# modified
|
||||
response = s.get(
|
||||
f"http://{ip}:{port}/test_resource/test_field",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), "TEST SET VALUE 2")
|
||||
|
||||
finally:
|
||||
proc.terminate()
|
||||
s.close()
|
||||
|
||||
@@ -14,6 +14,7 @@ print(__name__)
|
||||
print(__package__)
|
||||
|
||||
from src.pyrestresource import (
|
||||
RestResourceHandlerException_Forbiden,
|
||||
register_rest_rootpoint,
|
||||
RestResourceBase,
|
||||
rsrc_verb,
|
||||
@@ -21,6 +22,9 @@ from src.pyrestresource import (
|
||||
RestRequestParams_POST,
|
||||
RestRequestParams_Dict_GET,
|
||||
T_SupportedRESTFields,
|
||||
ACL_target_group_Any,
|
||||
ACL_record,
|
||||
ACL_rule,
|
||||
)
|
||||
from pprint import pprint
|
||||
|
||||
@@ -58,9 +62,19 @@ def init_classes():
|
||||
Patch_2 = Patch(uuid="d385a1d2-65fa-11ee-8c99-0242ac120002", shortname="testPatch2")
|
||||
|
||||
class User(RestResourceBase):
|
||||
uuid: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
uuid: UUID = Field(
|
||||
default_factory=uuid4,
|
||||
primary_key=True,
|
||||
)
|
||||
name: str
|
||||
secret: str = Field(..., exclude=True)
|
||||
secret: str = Field(
|
||||
...,
|
||||
exclude=True,
|
||||
ACL=[
|
||||
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.ALLOW),
|
||||
ACL_record(verbs=[rsrc_verb.GET], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
|
||||
],
|
||||
)
|
||||
|
||||
User1 = User(
|
||||
uuid="8da57a3c-661f-11ee-8c99-0242ac120002",
|
||||
@@ -68,8 +82,6 @@ def init_classes():
|
||||
secret="la blanquette est bonne",
|
||||
)
|
||||
|
||||
ext_patchs: dict[UUID, Patch] = {}
|
||||
|
||||
class Patch2(RestResourceBase):
|
||||
uuid: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
shortname: str
|
||||
@@ -117,100 +129,100 @@ class Test_RestAPI_GET(unittest.TestCase):
|
||||
|
||||
def test_get_root(self):
|
||||
result = self.testapp.process_request("/", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"testValueRoot": 3.14}')
|
||||
self.assertEqual(result.get_result(), '{"testValueRoot": 3.14}')
|
||||
|
||||
def test_get_root__multiple_slash(self):
|
||||
result = self.testapp.process_request("/////", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"testValueRoot": 3.14}')
|
||||
self.assertEqual(result.get_result(), '{"testValueRoot": 3.14}')
|
||||
|
||||
result = self.testapp.process_request("////", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"testValueRoot": 3.14}')
|
||||
self.assertEqual(result.get_result(), '{"testValueRoot": 3.14}')
|
||||
|
||||
def test_get_root__nested_value(self):
|
||||
result = self.testapp.process_request("/testValueRoot", rsrc_verb.GET)
|
||||
self.assertEqual(result, "3.14")
|
||||
self.assertEqual(result.get_result(), "3.14")
|
||||
|
||||
def test_get_root__nested_value__trailing_slash(self):
|
||||
result = self.testapp.process_request("/testValueRoot/", rsrc_verb.GET)
|
||||
self.assertEqual(result, "3.14")
|
||||
self.assertEqual(result.get_result(), "3.14")
|
||||
|
||||
result = self.testapp.process_request("/testValueRoot//", rsrc_verb.GET)
|
||||
self.assertEqual(result, "3.14")
|
||||
self.assertEqual(result.get_result(), "3.14")
|
||||
|
||||
result = self.testapp.process_request("/testValueRoot///", rsrc_verb.GET)
|
||||
self.assertEqual(result, "3.14")
|
||||
self.assertEqual(result.get_result(), "3.14")
|
||||
|
||||
def test_get_root__nested_value__multiple_slash(self):
|
||||
result = self.testapp.process_request("//testValueRoot", rsrc_verb.GET)
|
||||
self.assertEqual(result, "3.14")
|
||||
self.assertEqual(result.get_result(), "3.14")
|
||||
|
||||
result = self.testapp.process_request("///testValueRoot", rsrc_verb.GET)
|
||||
self.assertEqual(result, "3.14")
|
||||
self.assertEqual(result.get_result(), "3.14")
|
||||
|
||||
def test_get_version(self):
|
||||
result = self.testapp.process_request("/info", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
|
||||
def test_get_version__trailing_slash(self):
|
||||
result = self.testapp.process_request("/info/", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
|
||||
result = self.testapp.process_request("/info//", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
|
||||
result = self.testapp.process_request("/info///", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
|
||||
def test_get_version__multiple_slash(self):
|
||||
result = self.testapp.process_request("//info", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
|
||||
result = self.testapp.process_request("///info", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "0.0.1", "api_version": "0.0.2"}')
|
||||
|
||||
def test_get_version__nested_value(self):
|
||||
result = self.testapp.process_request("/info/api_version", rsrc_verb.GET)
|
||||
self.assertEqual(result, '"0.0.2"')
|
||||
self.assertEqual(result.get_result(), '"0.0.2"')
|
||||
|
||||
result = self.testapp.process_request("/info/version", rsrc_verb.GET)
|
||||
self.assertEqual(result, '"0.0.1"')
|
||||
self.assertEqual(result.get_result(), '"0.0.1"')
|
||||
|
||||
def test_get_dict_games(self):
|
||||
result = self.testapp.process_request("/games", rsrc_verb.GET)
|
||||
self.assertEqual(result, '["9b0381d4-65f6-11ee-8c99-0242ac120002"]')
|
||||
self.assertEqual(result.get_result(), '["9b0381d4-65f6-11ee-8c99-0242ac120002"]')
|
||||
|
||||
def test_get_dict_patchs(self):
|
||||
result = self.testapp.process_request("/patchs", rsrc_verb.GET)
|
||||
self.assertEqual(
|
||||
result,
|
||||
result.get_result(),
|
||||
'["cee1e870-65fa-11ee-8c99-0242ac120002", "d385a1d2-65fa-11ee-8c99-0242ac120002"]',
|
||||
)
|
||||
|
||||
def test_get_dict_patch_element(self):
|
||||
result = self.testapp.process_request("/patchs/cee1e870-65fa-11ee-8c99-0242ac120002", rsrc_verb.GET)
|
||||
self.assertEqual(
|
||||
result,
|
||||
result.get_result(),
|
||||
'{"uuid": "cee1e870-65fa-11ee-8c99-0242ac120002", "shortname": "testPatch1", "name": null, "description": null}',
|
||||
)
|
||||
|
||||
def test_get_dict_game_element(self):
|
||||
result = self.testapp.process_request("/games/9b0381d4-65f6-11ee-8c99-0242ac120002", rsrc_verb.GET)
|
||||
expected = '{"uuid": "9b0381d4-65f6-11ee-8c99-0242ac120002", "shortname": "testGame", "name": null, "description": null}'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_get_dict_game_element__nested_value(self):
|
||||
result = self.testapp.process_request("/games/9b0381d4-65f6-11ee-8c99-0242ac120002/shortname", rsrc_verb.GET)
|
||||
expected = '"testGame"'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_get_dict_game_element__nested_value2(self):
|
||||
result = self.testapp.process_request("/games/9b0381d4-65f6-11ee-8c99-0242ac120002/uuid", rsrc_verb.GET)
|
||||
expected = '"9b0381d4-65f6-11ee-8c99-0242ac120002"'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_get_nested_dict_games_patchs(self):
|
||||
result = self.testapp.process_request("/games/9b0381d4-65f6-11ee-8c99-0242ac120002/patchs", rsrc_verb.GET)
|
||||
self.assertEqual(result, '["cee1e870-65fa-11ee-8c99-0242ac120002"]')
|
||||
self.assertEqual(result.get_result(), '["cee1e870-65fa-11ee-8c99-0242ac120002"]')
|
||||
|
||||
def test_get_nested_dict_games_patch_element(self):
|
||||
result = self.testapp.process_request(
|
||||
@@ -218,28 +230,28 @@ class Test_RestAPI_GET(unittest.TestCase):
|
||||
rsrc_verb.GET,
|
||||
)
|
||||
expected = '{"uuid": "cee1e870-65fa-11ee-8c99-0242ac120002", "shortname": "testPatch1", "name": null, "description": null}'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_get_nested_dict_games_patch_element__nested_value(self):
|
||||
result = self.testapp.process_request(
|
||||
"/games/9b0381d4-65f6-11ee-8c99-0242ac120002/patchs/cee1e870-65fa-11ee-8c99-0242ac120002/uuid",
|
||||
rsrc_verb.GET,
|
||||
)
|
||||
self.assertEqual(result, '"cee1e870-65fa-11ee-8c99-0242ac120002"')
|
||||
self.assertEqual(result.get_result(), '"cee1e870-65fa-11ee-8c99-0242ac120002"')
|
||||
|
||||
def test_get_dict_game_element__API_nested(self):
|
||||
result = self.testapp.process_request("/games/9b0381d4-65f6-11ee-8c99-0242ac120002?API_nested=True", rsrc_verb.GET)
|
||||
expected = '{"uuid": "9b0381d4-65f6-11ee-8c99-0242ac120002", "shortname": "testGame", "name": null, "description": null}'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_get_dict_users(self):
|
||||
result = self.testapp.process_request("/users", rsrc_verb.GET)
|
||||
self.assertEqual(result, '["8da57a3c-661f-11ee-8c99-0242ac120002"]')
|
||||
self.assertEqual(result.get_result(), '["8da57a3c-661f-11ee-8c99-0242ac120002"]')
|
||||
|
||||
def test_get_dict_user_element(self):
|
||||
result = self.testapp.process_request("/users/8da57a3c-661f-11ee-8c99-0242ac120002", rsrc_verb.GET)
|
||||
self.assertEqual(
|
||||
result,
|
||||
result.get_result(),
|
||||
'{"uuid": "8da57a3c-661f-11ee-8c99-0242ac120002", "name": "chacha"}',
|
||||
"no secret seen",
|
||||
)
|
||||
@@ -247,21 +259,21 @@ class Test_RestAPI_GET(unittest.TestCase):
|
||||
def test_get_dict_user_element2(self):
|
||||
result = self.testapp.process_request("/users/8da57a3c-661f-11ee-8c99-0242ac120002?API_nested=True", rsrc_verb.GET)
|
||||
self.assertEqual(
|
||||
result,
|
||||
result.get_result(),
|
||||
'{"uuid": "8da57a3c-661f-11ee-8c99-0242ac120002", "name": "chacha"}',
|
||||
"no secret seen",
|
||||
)
|
||||
|
||||
def test_get_dict_user_element__nested_value(self):
|
||||
result = self.testapp.process_request("/users/8da57a3c-661f-11ee-8c99-0242ac120002/name", rsrc_verb.GET)
|
||||
self.assertEqual(result, '"chacha"')
|
||||
self.assertEqual(result.get_result(), '"chacha"')
|
||||
|
||||
def test_get_dict_user_element__nested_value__forbiden(self):
|
||||
with self.assertRaises(RuntimeError): # TODO: custom exception
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request("/users/8da57a3c-661f-11ee-8c99-0242ac120002/secret", rsrc_verb.GET)
|
||||
|
||||
def test_get_dict_user_element__nested_value__forbiden2(self):
|
||||
with self.assertRaises(RuntimeError): # TODO: custom exception
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request(
|
||||
"/users/8da57a3c-661f-11ee-8c99-0242ac120002/secret?API_nested=True",
|
||||
rsrc_verb.GET,
|
||||
@@ -278,7 +290,7 @@ class Test_RestAPI_PUT(unittest.TestCase):
|
||||
self.testapp.process_request("/info", rsrc_verb.PUT, '{"version": "1.2.3", "api_version": "3.2.1"}')
|
||||
|
||||
result = self.testapp.process_request("/info", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.2.3", "api_version": "3.2.1"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.2.3", "api_version": "3.2.1"}')
|
||||
|
||||
def test_put_dict_user_nested_value(self):
|
||||
self.testapp.process_request(
|
||||
@@ -288,12 +300,12 @@ class Test_RestAPI_PUT(unittest.TestCase):
|
||||
)
|
||||
|
||||
result = self.testapp.process_request("/users/8da57a3c-661f-11ee-8c99-0242ac120002/name", rsrc_verb.GET)
|
||||
self.assertEqual(result, '"chacha2"')
|
||||
self.assertEqual(result.get_result(), '"chacha2"')
|
||||
|
||||
def test_put_user_nested_value__forbiden(self):
|
||||
with self.assertRaises(RuntimeError): # TODO: custom exception
|
||||
with self.assertRaises(RestResourceHandlerException_Forbiden): # TODO: custom exception
|
||||
self.testapp.process_request(
|
||||
"/users/8da57a3c-661f-11ee-8c99-0242ac120002/secret",
|
||||
"/users/8da57a3c-661f-11ee-8c99-0242ac120002/uuid",
|
||||
rsrc_verb.PUT,
|
||||
'"test"',
|
||||
)
|
||||
@@ -307,11 +319,11 @@ class Test_RestAPI_PUT(unittest.TestCase):
|
||||
|
||||
result = self.testapp.process_request("/users", rsrc_verb.GET)
|
||||
expected = '["8da57a3c-661f-11ee-8c99-0242ac120002"]'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
result = self.testapp.process_request("/users/8da57a3c-661f-11ee-8c99-0242ac120002", rsrc_verb.GET)
|
||||
expected = '{"uuid": "8da57a3c-661f-11ee-8c99-0242ac120002", "name": "testUser4"}'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_put_dict_patch__nested(self):
|
||||
self.testapp.process_request(
|
||||
@@ -325,7 +337,7 @@ class Test_RestAPI_PUT(unittest.TestCase):
|
||||
rsrc_verb.GET,
|
||||
)
|
||||
expected = '{"uuid": "cee1e870-65fa-11ee-8c99-0242ac120002", "shortname": "testPatch998", "name": "MyPatch", "description": "MyDescription123"}'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
|
||||
class Test_RestAPI_POST(unittest.TestCase):
|
||||
@@ -340,15 +352,15 @@ class Test_RestAPI_POST(unittest.TestCase):
|
||||
rsrc_verb.POST,
|
||||
'{"name": "testUser", "secret": "test"}',
|
||||
)
|
||||
self.assertEqual(result, '"e5e87d32-662b-11ee-8c99-0242ac120002"')
|
||||
self.assertEqual(result.get_result(), '"e5e87d32-662b-11ee-8c99-0242ac120002"')
|
||||
|
||||
result = self.testapp.process_request("/users", rsrc_verb.GET)
|
||||
expected = '["8da57a3c-661f-11ee-8c99-0242ac120002", "e5e87d32-662b-11ee-8c99-0242ac120002"]'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
result = self.testapp.process_request("/users/e5e87d32-662b-11ee-8c99-0242ac120002", rsrc_verb.GET)
|
||||
expected = '{"uuid": "e5e87d32-662b-11ee-8c99-0242ac120002", "name": "testUser"}'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_post_dict_user__nested_key(self):
|
||||
result = self.testapp.process_request(
|
||||
@@ -356,15 +368,15 @@ class Test_RestAPI_POST(unittest.TestCase):
|
||||
rsrc_verb.POST,
|
||||
'{"name": "testUser2", "secret": "test", "uuid":"e7e86d32-662b-11ee-8c99-0242ac120002"}',
|
||||
)
|
||||
self.assertEqual(result, '"e7e86d32-662b-11ee-8c99-0242ac120002"')
|
||||
self.assertEqual(result.get_result(), '"e7e86d32-662b-11ee-8c99-0242ac120002"')
|
||||
|
||||
result = self.testapp.process_request("/users", rsrc_verb.GET)
|
||||
expected = '["8da57a3c-661f-11ee-8c99-0242ac120002", "e7e86d32-662b-11ee-8c99-0242ac120002"]'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
result = self.testapp.process_request("/users/e7e86d32-662b-11ee-8c99-0242ac120002", rsrc_verb.GET)
|
||||
expected = '{"uuid": "e7e86d32-662b-11ee-8c99-0242ac120002", "name": "testUser2"}'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
@patch(f"{__loader__.name }.uuid4")
|
||||
def test_post_dict_user__auto_key(self, mock_uuid4):
|
||||
@@ -375,15 +387,15 @@ class Test_RestAPI_POST(unittest.TestCase):
|
||||
self.testapp = RootApp()
|
||||
|
||||
result = self.testapp.process_request("/users", rsrc_verb.POST, '{"name": "testUser3", "secret": "test"}')
|
||||
self.assertEqual(result, '"5faccb2e-69aa-11ee-8c99-0242ac120002"')
|
||||
self.assertEqual(result.get_result(), '"5faccb2e-69aa-11ee-8c99-0242ac120002"')
|
||||
|
||||
result = self.testapp.process_request("/users", rsrc_verb.GET)
|
||||
expected = '["8da57a3c-661f-11ee-8c99-0242ac120002", "5faccb2e-69aa-11ee-8c99-0242ac120002"]'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
result = self.testapp.process_request("/users/5faccb2e-69aa-11ee-8c99-0242ac120002", rsrc_verb.GET)
|
||||
expected = '{"uuid": "5faccb2e-69aa-11ee-8c99-0242ac120002", "name": "testUser3"}'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_post_dict_patch__nested_API_key(self):
|
||||
self.testapp.process_request(
|
||||
@@ -397,7 +409,7 @@ class Test_RestAPI_POST(unittest.TestCase):
|
||||
rsrc_verb.GET,
|
||||
)
|
||||
expected = '{"uuid": "cee1e971-65fa-11ee-8c99-0242ac120002", "shortname": "testPatch99", "name": "MyPatch", "description": "MyDescription"}'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
|
||||
class Test_RestAPI_DELETE(unittest.TestCase):
|
||||
@@ -411,7 +423,7 @@ class Test_RestAPI_DELETE(unittest.TestCase):
|
||||
|
||||
result = self.testapp.process_request("/users", rsrc_verb.GET)
|
||||
expected = "[]"
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_delete_dict_user__All(self):
|
||||
result = self.testapp.process_request(
|
||||
@@ -419,24 +431,24 @@ class Test_RestAPI_DELETE(unittest.TestCase):
|
||||
rsrc_verb.POST,
|
||||
'{"name": "testUser", "secret": "test"}',
|
||||
)
|
||||
self.assertEqual(result, '"e5e87d32-662b-11ee-8c99-0242ac120002"')
|
||||
self.assertEqual(result.get_result(), '"e5e87d32-662b-11ee-8c99-0242ac120002"')
|
||||
|
||||
result = self.testapp.process_request("/users", rsrc_verb.GET)
|
||||
expected = '["8da57a3c-661f-11ee-8c99-0242ac120002", "e5e87d32-662b-11ee-8c99-0242ac120002"]'
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
self.testapp.process_request("/users", rsrc_verb.DELETE)
|
||||
|
||||
result = self.testapp.process_request("/users", rsrc_verb.GET)
|
||||
expected = "[]"
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_delete_dict_user_element(self):
|
||||
self.testapp.process_request("/users/8da57a3c-661f-11ee-8c99-0242ac120002", rsrc_verb.DELETE)
|
||||
|
||||
result = self.testapp.process_request("/users", rsrc_verb.GET)
|
||||
expected = "[]"
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_delete_nested_dict_games_patch_element(self):
|
||||
self.testapp.process_request(
|
||||
@@ -446,7 +458,7 @@ class Test_RestAPI_DELETE(unittest.TestCase):
|
||||
|
||||
result = self.testapp.process_request("/games/9b0381d4-65f6-11ee-8c99-0242ac120002/patchs", rsrc_verb.GET)
|
||||
expected = "[]"
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_delete_nested_dict_games_patch_API_key(self):
|
||||
self.testapp.process_request(
|
||||
@@ -456,14 +468,14 @@ class Test_RestAPI_DELETE(unittest.TestCase):
|
||||
|
||||
result = self.testapp.process_request("/games/9b0381d4-65f6-11ee-8c99-0242ac120002/patchs", rsrc_verb.GET)
|
||||
expected = "[]"
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
def test_delete_nested_dict_games_patch_All(self):
|
||||
self.testapp.process_request("/games/9b0381d4-65f6-11ee-8c99-0242ac120002/patchs", rsrc_verb.DELETE)
|
||||
|
||||
result = self.testapp.process_request("/games/9b0381d4-65f6-11ee-8c99-0242ac120002/patchs", rsrc_verb.GET)
|
||||
expected = "[]"
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(result.get_result(), expected)
|
||||
|
||||
|
||||
class Test_RestAPI_PERFO(unittest.TestCase):
|
||||
@@ -472,19 +484,19 @@ class Test_RestAPI_PERFO(unittest.TestCase):
|
||||
init_classes()
|
||||
self.testapp = RootApp()
|
||||
|
||||
@unittest.skip
|
||||
# @unittest.skip
|
||||
def test_perf_dict(self):
|
||||
print(f"LIB INTERNAL PERF TEST")
|
||||
n_loop = 10000
|
||||
|
||||
start = time()
|
||||
for i in range(n_loop):
|
||||
for _ in range(n_loop):
|
||||
self.testapp.process_request(f"/users/8da57a3c-661f-11ee-8c99-0242ac120002", rsrc_verb.GET)
|
||||
end = time()
|
||||
print(f"GET 1st level dict: {int(n_loop/(end-start))} Req/s")
|
||||
|
||||
start = time()
|
||||
for i in range(n_loop):
|
||||
for _ in range(n_loop):
|
||||
newUUID = uuid4()
|
||||
self.testapp.process_request(
|
||||
f"/users?API_key={newUUID}",
|
||||
@@ -495,7 +507,7 @@ class Test_RestAPI_PERFO(unittest.TestCase):
|
||||
print(f"POST 1st level dict (API_key): {int(n_loop/(end-start))} Req/s")
|
||||
|
||||
start = time()
|
||||
for i in range(n_loop):
|
||||
for _ in range(n_loop):
|
||||
newUUID = uuid4()
|
||||
self.testapp.process_request(
|
||||
f"/users?API_key={newUUID}",
|
||||
@@ -507,14 +519,14 @@ class Test_RestAPI_PERFO(unittest.TestCase):
|
||||
print(f"POST/GET 1st level dict (API_key): {int(n_loop/(end-start))} Req/s")
|
||||
|
||||
start = time()
|
||||
for i in range(n_loop):
|
||||
for _ in range(n_loop):
|
||||
result = self.testapp.process_request(f"/users", rsrc_verb.POST, '{"name": "testUser", "secret": "test"}')
|
||||
self.testapp.process_request(f"/users/{json.loads(result)}", rsrc_verb.GET)
|
||||
self.testapp.process_request(f"/users/{json.loads(result.get_result())}", rsrc_verb.GET)
|
||||
end = time()
|
||||
print(f"POST/GET 1st level dict (autokey): {int(n_loop/(end-start))} Req/s")
|
||||
|
||||
start = time()
|
||||
for i in range(n_loop):
|
||||
for _ in range(n_loop):
|
||||
self.testapp.process_request(
|
||||
f"/games/9b0381d4-65f6-11ee-8c99-0242ac120002/shortname",
|
||||
rsrc_verb.PUT,
|
||||
@@ -525,7 +537,7 @@ class Test_RestAPI_PERFO(unittest.TestCase):
|
||||
print(f"PUT/GET 1st level (value) dict: {int(n_loop/(end-start))} Req/s")
|
||||
|
||||
start = time()
|
||||
for i in range(n_loop):
|
||||
for _ in range(n_loop):
|
||||
self.testapp.process_request(
|
||||
f"/games/9b0381d4-65f6-11ee-8c99-0242ac120002/patchs/cee1e870-65fa-11ee-8c99-0242ac120002",
|
||||
rsrc_verb.GET,
|
||||
@@ -534,7 +546,7 @@ class Test_RestAPI_PERFO(unittest.TestCase):
|
||||
print(f"GET 2nd level dict: {int(n_loop/(end-start))} Req/s")
|
||||
|
||||
start = time()
|
||||
for i in range(n_loop):
|
||||
for _ in range(n_loop):
|
||||
self.testapp.process_request(
|
||||
f"/games/9b0381d4-65f6-11ee-8c99-0242ac120002/patchs/cee1e870-65fa-11ee-8c99-0242ac120002/shortname",
|
||||
rsrc_verb.GET,
|
||||
@@ -543,7 +555,7 @@ class Test_RestAPI_PERFO(unittest.TestCase):
|
||||
print(f"GET 2nd level (value) dict: {int(n_loop/(end-start))} Req/s")
|
||||
|
||||
start = time()
|
||||
for i in range(n_loop):
|
||||
for _ in range(n_loop):
|
||||
self.testapp.process_request(
|
||||
f"/games/9b0381d4-65f6-11ee-8c99-0242ac120002/patchs/cee1e870-65fa-11ee-8c99-0242ac120002/shortname",
|
||||
rsrc_verb.PUT,
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
from __future__ import annotations
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from os import chdir
|
||||
from pathlib import Path
|
||||
from typing import Optional, Annotated
|
||||
from typing import Annotated
|
||||
from pydantic import Field
|
||||
from uuid import UUID, uuid4
|
||||
from time import time
|
||||
import json
|
||||
|
||||
print(__name__)
|
||||
print(__package__)
|
||||
|
||||
from src.pyrestresource import (
|
||||
register_rest_rootpoint,
|
||||
@@ -23,8 +16,8 @@ from src.pyrestresource import (
|
||||
T_SupportedRESTFields,
|
||||
ResourcePlugin_field_default,
|
||||
ResourcePlugin_RestResourceBase_default,
|
||||
RestResourcePluginException_InvalidPluginSignature,
|
||||
)
|
||||
from pprint import pprint
|
||||
|
||||
testdir_path = Path(__file__).parent.resolve()
|
||||
chdir(testdir_path.parent.resolve())
|
||||
@@ -77,41 +70,9 @@ def init_classes():
|
||||
|
||||
|
||||
def init_bad_plugin1():
|
||||
# plugin with missing handle_resource_put() method
|
||||
# plugin not inheriting from the right base type
|
||||
class ResourcePlugin_TestResource:
|
||||
def handle_field_get(self, resource: TestResource, params: RestRequestParams_GET) -> TestResource:
|
||||
return resource
|
||||
|
||||
class TestResource(RestResourceBase):
|
||||
tetvaluestr: Annotated[str, Field(plugin=ResourcePlugin_TestResource)]
|
||||
|
||||
@register_rest_rootpoint
|
||||
class RootApp2(RestResourceBase):
|
||||
test: TestResource = Field(default=TestResource(tetvaluestr="testvalue"))
|
||||
|
||||
RootApp2()
|
||||
|
||||
|
||||
def init_bad_plugin2():
|
||||
# plugin with missing handle_resource_get() method
|
||||
class ResourcePlugin_TestResource:
|
||||
def handle_field_put(self, resource: TestResource, params: RestRequestParams_PUT) -> TestResource:
|
||||
return resource
|
||||
|
||||
class TestResource(RestResourceBase):
|
||||
tetvaluestr: Annotated[str, Field(plugin=ResourcePlugin_TestResource)]
|
||||
|
||||
@register_rest_rootpoint
|
||||
class RootApp2(RestResourceBase):
|
||||
test: TestResource = Field(default=TestResource(tetvaluestr="testvalue"))
|
||||
|
||||
RootApp2()
|
||||
|
||||
|
||||
def init_bad_plugin3():
|
||||
# wrong plugin
|
||||
class ResourcePlugin_TestResource(ResourcePlugin_RestResourceBase_default):
|
||||
pass
|
||||
...
|
||||
|
||||
class TestResource(RestResourceBase):
|
||||
tetvaluestr: Annotated[str, Field(plugin=ResourcePlugin_TestResource)]
|
||||
@@ -133,16 +94,17 @@ class Test_RestAPI_Plugin_PUT(unittest.TestCase):
|
||||
self.testapp.process_request("/info_put/version", rsrc_verb.PUT, '"1.5.6"')
|
||||
|
||||
result = self.testapp.process_request("/info_put", rsrc_verb.GET)
|
||||
print(result)
|
||||
self.assertEqual(result.get_result(), '{"version": "42", "api_version": "0.0.2"}')
|
||||
|
||||
result = self.testapp.process_request("/info_put/version", rsrc_verb.GET)
|
||||
print(result)
|
||||
self.assertEqual(result, '"42"')
|
||||
|
||||
self.assertEqual(result.get_result(), '"42"')
|
||||
|
||||
def test_put_field_version_resourceplugin(self):
|
||||
self.testapp.process_request("/info_put", rsrc_verb.PUT, '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
|
||||
result = self.testapp.process_request("/info_put", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "42", "api_version": "98.321"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "42", "api_version": "98.321"}')
|
||||
|
||||
|
||||
class Test_RestAPI_Plugin_GET(unittest.TestCase):
|
||||
@@ -153,64 +115,60 @@ class Test_RestAPI_Plugin_GET(unittest.TestCase):
|
||||
|
||||
def test_get_root(self):
|
||||
result = self.testapp.process_request("/", rsrc_verb.GET)
|
||||
self.assertEqual(result, "{}")
|
||||
self.assertEqual(result.get_result(), "{}")
|
||||
|
||||
def test_get_version(self):
|
||||
result = self.testapp.process_request("/info", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
|
||||
result = self.testapp.process_request("/info2", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
|
||||
def test_get_version__trailing_slash(self):
|
||||
result = self.testapp.process_request("/info/", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
|
||||
result = self.testapp.process_request("/info//", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
|
||||
result = self.testapp.process_request("/info///", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
|
||||
result = self.testapp.process_request("/info2/", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
|
||||
result = self.testapp.process_request("/info2//", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
|
||||
result = self.testapp.process_request("/info2///", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
|
||||
def test_get_version__multiple_slash(self):
|
||||
result = self.testapp.process_request("//info", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
|
||||
result = self.testapp.process_request("///info", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "98.321"}')
|
||||
|
||||
result = self.testapp.process_request("//info2", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
|
||||
result = self.testapp.process_request("///info2", rsrc_verb.GET)
|
||||
self.assertEqual(result, '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
self.assertEqual(result.get_result(), '{"version": "1.5.6", "api_version": "0.0.3"}')
|
||||
|
||||
def test_get_version__nested_value(self):
|
||||
result = self.testapp.process_request("/info/api_version", rsrc_verb.GET)
|
||||
self.assertEqual(result, '"98.321"')
|
||||
self.assertEqual(result.get_result(), '"98.321"')
|
||||
|
||||
result = self.testapp.process_request("/info/version", rsrc_verb.GET)
|
||||
self.assertEqual(result, '"1.5.6"')
|
||||
self.assertEqual(result.get_result(), '"1.5.6"')
|
||||
|
||||
result = self.testapp.process_request("/info2/api_version", rsrc_verb.GET)
|
||||
self.assertEqual(result, '"0.0.3"')
|
||||
self.assertEqual(result.get_result(), '"0.0.3"')
|
||||
|
||||
result = self.testapp.process_request("/info2/version", rsrc_verb.GET)
|
||||
self.assertEqual(result, '"1.5.6"')
|
||||
self.assertEqual(result.get_result(), '"1.5.6"')
|
||||
|
||||
def test_defect_plugin_field(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
with self.assertRaises(RestResourcePluginException_InvalidPluginSignature):
|
||||
init_bad_plugin1()
|
||||
with self.assertRaises(RuntimeError):
|
||||
init_bad_plugin2()
|
||||
with self.assertRaises(RuntimeError):
|
||||
init_bad_plugin3()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
|
||||
from os import chdir
|
||||
from pathlib import Path
|
||||
from pydantic import Field
|
||||
from io import StringIO
|
||||
from contextlib import redirect_stdout, redirect_stderr
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
print(__name__)
|
||||
print(__package__)
|
||||
@@ -28,50 +28,39 @@ chdir(testdir_path.parent.resolve())
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_T_Dict_TEST_Print(RestResourceWalker_Sub_T_Dict):
|
||||
counter: dict[str, int] = {}
|
||||
|
||||
@classmethod
|
||||
def init_sub(cls, walker: RestResourceWalker_Root) -> None:
|
||||
cls.counter = {}
|
||||
cls_counter: dict[str, int] = {}
|
||||
|
||||
def process(self) -> None:
|
||||
if self.resource_name not in self.counter:
|
||||
self.counter[self.resource_name] = 0
|
||||
self.counter[self.resource_name] = self.counter[self.resource_name] + 1
|
||||
counter = self.cls_counter
|
||||
if self.resource_name not in counter:
|
||||
counter[self.resource_name] = 0
|
||||
counter[self.resource_name] = counter[self.resource_name] + 1
|
||||
|
||||
print(f"DICT {self.resource_name} {self.counter[self.resource_name]}")
|
||||
print(f"DICT {self.resource_name} {counter[self.resource_name]}")
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_RestFields_TEST_Print(RestResourceWalker_Sub_RestFields):
|
||||
counter: dict[str, int] = {}
|
||||
|
||||
@classmethod
|
||||
def init_sub(cls, walker: RestResourceWalker_Root) -> None:
|
||||
cls.counter = {}
|
||||
cls_counter: dict[str, int] = {}
|
||||
|
||||
def process(self) -> None:
|
||||
if self.resource_name not in self.counter:
|
||||
self.counter[self.resource_name] = 0
|
||||
self.counter[self.resource_name] = self.counter[self.resource_name] + 1
|
||||
counter = self.cls_counter
|
||||
if self.resource_name not in counter:
|
||||
counter[self.resource_name] = 0
|
||||
counter[self.resource_name] = counter[self.resource_name] + 1
|
||||
|
||||
print(f"FIELD {self.resource_name} {self.counter[self.resource_name]}")
|
||||
print(f"FIELD {self.resource_name} {counter[self.resource_name]}")
|
||||
|
||||
|
||||
class RestResourceWalker_Sub_RestResourceBase_TEST_Print(
|
||||
RestResourceWalker_Sub_RestResourceBase
|
||||
):
|
||||
counter: dict[str, int] = {}
|
||||
|
||||
@classmethod
|
||||
def init_sub(cls, walker: RestResourceWalker_Root) -> None:
|
||||
cls.counter = {}
|
||||
class RestResourceWalker_Sub_RestResourceBase_TEST_Print(RestResourceWalker_Sub_RestResourceBase):
|
||||
cls_counter: dict[str, int] = {}
|
||||
|
||||
def process(self) -> None:
|
||||
if self.resource_name not in self.counter:
|
||||
self.counter[self.resource_name] = 0
|
||||
self.counter[self.resource_name] = self.counter[self.resource_name] + 1
|
||||
counter = self.cls_counter
|
||||
if self.resource_name not in counter:
|
||||
counter[self.resource_name] = 0
|
||||
counter[self.resource_name] = counter[self.resource_name] + 1
|
||||
|
||||
print(f"RestResource {self.resource_name} {self.counter[self.resource_name]}")
|
||||
print(f"RestResource {self.resource_name} {counter[self.resource_name]}")
|
||||
|
||||
|
||||
class RestResourceWalker_Root_TEST_Print(RestResourceWalker_Root):
|
||||
@@ -114,11 +103,12 @@ class Test_Walker(unittest.TestCase):
|
||||
init_classes()
|
||||
|
||||
def test_walk_class(self):
|
||||
RestResourceWalker_Sub_T_Dict_TEST_Print.cls_counter = {}
|
||||
RestResourceWalker_Sub_RestFields_TEST_Print.cls_counter = {}
|
||||
RestResourceWalker_Sub_RestResourceBase_TEST_Print.cls_counter = {}
|
||||
test = RestResourceWalker_Root_TEST_Print(RootApp)
|
||||
with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(
|
||||
StringIO()
|
||||
) as capted_stderr:
|
||||
test.process()
|
||||
with redirect_stdout(StringIO()) as capted_stdout:
|
||||
test.process({})
|
||||
self.assertIn("RestResource info 1", capted_stdout.getvalue())
|
||||
self.assertIn("RestResource info2 1", capted_stdout.getvalue())
|
||||
self.assertIn("DICT peoples 1", capted_stdout.getvalue())
|
||||
@@ -133,12 +123,13 @@ class Test_Walker(unittest.TestCase):
|
||||
self.assertIn("FIELD last_name 1", capted_stdout.getvalue())
|
||||
|
||||
def test_walk_obj(self):
|
||||
RestResourceWalker_Sub_T_Dict_TEST_Print.cls_counter = {}
|
||||
RestResourceWalker_Sub_RestFields_TEST_Print.cls_counter = {}
|
||||
RestResourceWalker_Sub_RestResourceBase_TEST_Print.cls_counter = {}
|
||||
instRootApp = RootApp()
|
||||
test = RestResourceWalker_Root_TEST_Print(instRootApp)
|
||||
with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(
|
||||
StringIO()
|
||||
) as capted_stderr:
|
||||
test.process()
|
||||
with redirect_stdout(StringIO()) as capted_stdout:
|
||||
test.process({})
|
||||
self.assertIn("RestResource info 1", capted_stdout.getvalue())
|
||||
self.assertIn("RestResource info2 1", capted_stdout.getvalue())
|
||||
self.assertIn("DICT peoples 1", capted_stdout.getvalue())
|
||||
@@ -153,11 +144,12 @@ class Test_Walker(unittest.TestCase):
|
||||
self.assertIn("FIELD last_name 1", capted_stdout.getvalue())
|
||||
|
||||
def test_walk_obj_nested_RestResource(self):
|
||||
RestResourceWalker_Sub_T_Dict_TEST_Print.cls_counter = {}
|
||||
RestResourceWalker_Sub_RestFields_TEST_Print.cls_counter = {}
|
||||
RestResourceWalker_Sub_RestResourceBase_TEST_Print.cls_counter = {}
|
||||
instRootApp = RootApp()
|
||||
test = RestResourceWalker_Root_TEST_Print(instRootApp.info)
|
||||
with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(
|
||||
StringIO()
|
||||
) as capted_stderr:
|
||||
test.process()
|
||||
with redirect_stdout(StringIO()) as capted_stdout:
|
||||
test.process({})
|
||||
self.assertIn("FIELD version 1", capted_stdout.getvalue())
|
||||
self.assertIn("FIELD api_version 1", capted_stdout.getvalue())
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
from typing import Annotated, Optional
|
||||
from typing import Optional
|
||||
|
||||
from os import chdir
|
||||
from pathlib import Path
|
||||
from pydantic import Field
|
||||
from io import StringIO
|
||||
from contextlib import redirect_stdout, redirect_stderr
|
||||
|
||||
|
||||
print(__name__)
|
||||
print(__package__)
|
||||
|
||||
@@ -13,7 +13,7 @@ import socket
|
||||
import requests
|
||||
from contextlib import closing
|
||||
from multiprocessing import Process
|
||||
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
print(__name__)
|
||||
print(__package__)
|
||||
@@ -117,13 +117,10 @@ def find_free_port():
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||||
s.bind(("", 0))
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
hostname = socket.gethostname()
|
||||
IPAddr = socket.gethostbyname(hostname)
|
||||
return "localhost", s.getsockname()[1]
|
||||
|
||||
|
||||
def launch_server(ip, port):
|
||||
print(f"port2={port}")
|
||||
init_classes()
|
||||
uvicorn.run(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True)
|
||||
|
||||
@@ -134,8 +131,6 @@ class Test_RestAPI_WebServer(unittest.TestCase):
|
||||
|
||||
def test_nomal_AllCmd_games(self):
|
||||
ip, port = find_free_port()
|
||||
print(f"ip1={ip}")
|
||||
print(f"port1={port}")
|
||||
proc = Process(
|
||||
target=launch_server,
|
||||
args=(
|
||||
@@ -146,6 +141,8 @@ class Test_RestAPI_WebServer(unittest.TestCase):
|
||||
proc.start()
|
||||
sleep(1)
|
||||
s = requests.Session()
|
||||
s.mount("http://", HTTPAdapter(max_retries=0))
|
||||
|
||||
try:
|
||||
# Fetching games
|
||||
response = s.get(f"http://{ip}:{port}/games")
|
||||
@@ -157,15 +154,6 @@ class Test_RestAPI_WebServer(unittest.TestCase):
|
||||
["9b0381d4-65f6-11ee-8c99-0242ac120002"],
|
||||
)
|
||||
|
||||
# Login in
|
||||
"""
|
||||
response = s.post(
|
||||
f"http://{ip}:{port}/login",
|
||||
params={"username": "test", "password": "test"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
"""
|
||||
|
||||
# Add a new one (with all values setted)
|
||||
response = s.post(
|
||||
f"http://{ip}:{port}/games",
|
||||
@@ -287,14 +275,12 @@ class Test_RestAPI_WebServer(unittest.TestCase):
|
||||
proc.terminate()
|
||||
s.close()
|
||||
|
||||
@unittest.skip
|
||||
# @unittest.skip
|
||||
def test_perf_dict(self):
|
||||
print(f"SOCKET PERF TEST")
|
||||
n_loop = 10000
|
||||
|
||||
ip, port = find_free_port()
|
||||
print(f"ip1={ip}")
|
||||
print(f"port1={port}")
|
||||
proc = Process(
|
||||
target=launch_server,
|
||||
args=(
|
||||
@@ -305,6 +291,8 @@ class Test_RestAPI_WebServer(unittest.TestCase):
|
||||
proc.start()
|
||||
sleep(1)
|
||||
s = requests.Session()
|
||||
s.mount("http://", HTTPAdapter(max_retries=0))
|
||||
|
||||
try:
|
||||
start = time()
|
||||
for _ in range(n_loop):
|
||||
|
||||
Reference in New Issue
Block a user