5 Commits

Author SHA1 Message Date
cclecle
afaebec633 complete base dict plugin + unittest 2023-11-11 15:04:47 +00:00
cclecle
f677f7bf28 fix dict delete + plugin 2023-11-11 12:09:03 +00:00
cclecle
6cc6056220 start unittesting dict & add some fixes
allocate plugin at app creation, not at call to gain time and keep
context.
2023-11-11 11:49:51 +00:00
cclecle
de71a19956 fix import order in unittest 2023-11-11 10:03:01 +00:00
cclecle
3b358ab49c remove unused resource_handler_walker
uniform all if TYPE_CHECKING
ignore TYPE_CHECKING in coverage
2023-11-11 09:01:38 +00:00
21 changed files with 472 additions and 249 deletions

View File

@@ -3,5 +3,4 @@ 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

View File

@@ -64,6 +64,11 @@ concurrency = [
'thread'
]
[tool.coverage.report]
exclude_also = [
"if TYPE_CHECKING:",
]
[project.urls]
Homepage = "https://chacha.ddns.net/gitea/chacha/pyrestresource"
Documentation = "https://chacha.ddns.net/mkdocs-web/chacha/pyrestresource/master/latest/"

View File

@@ -31,6 +31,8 @@ from .rest_request_opt import (
RestRequestParams_Dict_POST,
RestRequestParams_Dict_DELETE,
RestRequestParams_Dict_GET,
RestRequestParams_Dict_elem_GET,
RestRequestParams_Dict_elem_PUT,
)
from .rest_resource_plugin import (
ResourcePlugin_field_default,

View File

@@ -6,7 +6,7 @@ from enum import Enum, auto
from .rest_types import rsrc_verb
if TYPE_CHECKING is True:
if TYPE_CHECKING:
from .rest_login import UserLogin

View File

@@ -30,7 +30,7 @@ from .rest_exceptions import (
RestResourceLoginException_InvalidSession,
)
if TYPE_CHECKING is True:
if TYPE_CHECKING:
from .rest_request import RestRequest
from .rest_request_opt import RestRequestParams_RestResourceBase_PUT, RestRequestParams_RestResourceBase_GET

View File

@@ -1,17 +1,11 @@
from __future__ import annotations
from typing import (
Any,
Literal,
Callable,
Optional,
TYPE_CHECKING,
)
from typing import Literal, Any, Callable, TYPE_CHECKING
from pydantic.fields import Field, _Unset, PydanticUndefined
from .rest_exceptions import RestResourceModelException
if TYPE_CHECKING is True:
if TYPE_CHECKING:
from .rest_ACL import ACL_record
from .rest_resource_plugin import ResourcePlugin
from typing import Unpack
@@ -24,11 +18,11 @@ def RestField(
default_factory: Callable[[], Any] | None = _Unset,
alias: str | None = _Unset,
alias_priority: int | None = _Unset,
validation_alias: str | AliasPath | AliasChoices | None = _Unset,
validation_alias: str | "AliasPath" | "AliasChoices" | None = _Unset,
serialization_alias: str | None = _Unset,
title: str | None = _Unset,
description: str | None = _Unset,
examples: list[Any] | None = _Unset,
examples: "list[Any] | None" = _Unset,
exclude: bool | None = _Unset,
discriminator: str | None = _Unset,
json_schema_extra: dict[str, Any] | Callable[[dict[str, Any]], None] | None = _Unset,
@@ -50,8 +44,8 @@ def RestField(
min_length: int | None = _Unset,
max_length: int | None = _Unset,
union_mode: Literal["smart", "left_to_right"] = _Unset,
ACL: Optional[list[ACL_record]] = _Unset,
plugin: Optional[type[ResourcePlugin]] = _Unset,
ACL: list["ACL_record"] | None = _Unset,
plugin: type["ResourcePlugin"] | None = _Unset,
**extra: Unpack[_EmptyKwargs],
) -> Any:
if not json_schema_extra or json_schema_extra is _Unset:

View File

@@ -35,7 +35,7 @@ from .rest_exceptions import (
RestResourceConfigException,
)
if TYPE_CHECKING is True:
if TYPE_CHECKING:
from typing import Optional
from .rest_types import T_SupportedRESTFields
from .rest_resource import RestResourceBase

View File

@@ -9,7 +9,7 @@ from .rest_types import (
_T_DictKey,
)
if TYPE_CHECKING is True:
if TYPE_CHECKING:
pass

View File

@@ -8,7 +8,7 @@ from typing import (
from abc import ABC
import json
import pprint
from pydantic import BaseModel
@@ -35,7 +35,7 @@ from .rest_exceptions import (
RestResourceException,
)
if TYPE_CHECKING is True:
if TYPE_CHECKING:
from .rest_request import RestRequest
from .rest_types import T_SupportedRESTFields
from .rest_resource_plugin import ResourcePlugin
@@ -53,7 +53,7 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
_plugins_: ClassVar[
dict[
str,
type[ResourcePlugin],
ResourcePlugin,
]
] = {}
_ACL_record_: ClassVar[

View File

@@ -43,7 +43,7 @@ from .rest_exceptions import (
RestResourceHandlerException_Forbiden,
)
if TYPE_CHECKING is True:
if TYPE_CHECKING:
from .rest_types import T_T_DictKey, T_T_DictValues
from .rest_request import RestRequest
@@ -292,6 +292,14 @@ class ResourceHandler_dict(
_dict: dict[T_DictKey, T_DictValues] = cast(dict[T_DictKey, T_DictValues], self.resource)
if self.prev_handler is not None and self.req.get_resource_origin(1) in self.prev_handler.resource._plugins_:
plugin_dict: ResourcePlugin_dict = cast(
ResourcePlugin_dict,
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)],
)
plugin_dict.set_context(self.req, self.req.get_root_resource())
return plugin_dict.handle_dict_get_keys(self.resource, params)
return list(_dict.keys())
def _handle_process_delete(self, params) -> None:
@@ -303,10 +311,33 @@ class ResourceHandler_dict(
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_key: T_DictKey
plugin_dict: ResourcePlugin_dict | None = None
if self.req.get_resource_origin(1) in self.prev_handler.resource._plugins_:
plugin_dict = cast(
ResourcePlugin_dict,
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)],
)
plugin_dict.set_context(self.req, self.req.get_root_resource())
if params.API_key is not None:
del _dict[dict_key_type(params.API_key)]
if issubclass(dict_key_type, bytes):
key_byte = dict_key_type(params.API_key, "utf-8")
_dict_key = key_byte
else:
key_std = dict_key_type(params.API_key)
_dict_key = key_std
if plugin_dict:
plugin_dict.handle_dict_delete(_dict, _dict_key, params)
return None
del _dict[_dict_key]
else:
if plugin_dict:
plugin_dict.handle_dict_delete_all(_dict, params)
return None
_dict.clear()
return
@@ -325,38 +356,64 @@ class ResourceHandler_dict(
]
_obj: T_DictValues
if not issubclass(dict_value_type, NoneType): # type: ignore # => mypy bug with type[None]
_obj = dict_value_type(**self.req.get_data()) # type: ignore # => mypy bug with type[None]
if issubclass(dict_value_type, RestResourceBase):
_obj = dict_value_type(**self.req.get_data())
_obj_restrsrc = cast(RestResourceBase, _obj)
for key, _ in _obj_restrsrc.model_fields.items():
if key in _obj_restrsrc._plugins_:
if isinstance(_obj_restrsrc._plugins_[key], ResourcePlugin_field):
plugin_field: ResourcePlugin_field = cast(ResourcePlugin_field, _obj_restrsrc._plugins_[key])
plugin_field.set_context(self.req, self.req.get_root_resource())
value = getattr(_obj_restrsrc, key)
setattr(_obj_restrsrc, key, plugin_field.handle_field_put(value, params))
elif not issubclass(dict_value_type, NoneType): # type: ignore # => mypy bug with Type[None]
_obj = dict_value_type(**self.req.get_data()) # type: ignore # => mypy bug with Type[None]
else:
_obj = None
_dict: dict[T_DictKey, T_DictValues] = cast(dict[T_DictKey, T_DictValues], self.resource)
_dict_key: T_DictKey | None = None
# 1st try/ using request param provided dict API_key
if params.API_key is not None:
if issubclass(dict_key_type, bytes):
key_byte: bytes = dict_key_type(params.API_key, "utf-8")
_dict_key = key_byte
else:
key_std = dict_key_type(params.API_key)
_dict_key = key_std
# if a primary key is set for the resource, updating it
if isinstance(_obj, RestResourceBase):
if _obj._primary_key_ is not None:
_pri: T_DictKey = dict_key_type(params.API_key)
setattr(_obj, _obj._primary_key_, _pri)
# storing resource
_dict[dict_key_type(params.API_key)] = _obj
return dict_key_type(params.API_key)
setattr(_obj, _obj._primary_key_, _dict_key)
# 2nd try/ using provided resource internal primary key
# & 3rd try/ using resource internal auto-generated primary key
# => this case is automatic because if self.req.get_data() doesn't contain the key, it should be automatically created
if isinstance(_obj, RestResourceBase):
elif isinstance(_obj, RestResourceBase):
if _obj._primary_key_ is not None:
_obj_primary_key: Optional[T_DictKey] = getattr(_obj, _obj._primary_key_)
if _obj_primary_key is not None:
_dict[_obj_primary_key] = _obj
return _obj_primary_key
_dict_key = _obj_primary_key
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....
if _dict_key is not None:
if self.req.get_resource_origin(1) in self.prev_handler.resource._plugins_:
plugin_dict: ResourcePlugin_dict = cast(
ResourcePlugin_dict,
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)],
)
plugin_dict.set_context(self.req, self.req.get_root_resource())
return plugin_dict.handle_dict_post(_dict, _dict_key, _obj, params)
else:
_dict[_dict_key] = _obj
return _dict_key
else:
raise RestResourceHandlerException_BadRequest(
"Either the object needs defined primary key or the request must contain an API_key param to process this command"
)
@ResourceHandler.register_resource_handler
@@ -396,15 +453,28 @@ class ResourceHandler_dict_elem(
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
assert self.prev_handler is not None
assert isinstance(self.prev_handler.resource, RestResourceBase)
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(1)]
dict_key_type: T_T_DictKey = 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_key: T_DictKey
if issubclass(dict_key_type, bytes):
key_byte = dict_key_type(self.req.get_resource_origin(0), "utf-8")
return cast(dict[T_DictKey, T_DictValues], self.resource)[key_byte]
_dict_key = key_byte
else:
key = dict_key_type(self.req.get_resource_origin(0))
return cast(dict[T_DictKey, T_DictValues], self.resource)[key]
key_std = dict_key_type(self.req.get_resource_origin(0))
_dict_key = key_std
if self.req.get_resource_origin(1) in self.prev_handler.resource._plugins_:
plugin_dict: ResourcePlugin_dict = cast(
ResourcePlugin_dict,
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)],
)
plugin_dict.set_context(self.req, self.req.get_root_resource())
return plugin_dict.handle_dict_elem_get(_dict, _dict_key, params)
return _dict[_dict_key]
def _handle_process_delete(self, params) -> None:
# print(f"{type(self).__name__}->_handle_process_delete()")
@@ -415,15 +485,28 @@ class ResourceHandler_dict_elem(
# because self.req is another context that is not saved to improve performances
assert self.prev_handler is not None
assert isinstance(self.prev_handler.resource, RestResourceBase)
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(2)]
dict_key_type: T_T_DictKey = self.prev_handler.resource._dict_key_type_[self.req.get_resource_origin(2)]
_dict: dict[T_DictKey, T_DictValues] = cast(dict[T_DictKey, T_DictValues], self.resource)
_dict_key: T_DictKey
if issubclass(dict_key_type, bytes):
key_byte = dict_key_type(self.req.get_resource_origin(1), "utf-8")
del cast(dict[T_DictKey, T_DictValues], self.resource)[key_byte]
_dict_key = key_byte
else:
key = dict_key_type(self.req.get_resource_origin(1))
del cast(dict[T_DictKey, T_DictValues], self.resource)[key]
key_std = dict_key_type(self.req.get_resource_origin(1))
_dict_key = key_std
if self.req.get_resource_origin(2) in self.prev_handler.resource._plugins_:
plugin_dict: ResourcePlugin_dict = cast(
ResourcePlugin_dict, self.prev_handler.resource._plugins_[self.req.get_resource_origin(2)]
)
plugin_dict.set_context(self.req, self.req.get_root_resource())
plugin_dict.handle_dict_delete(_dict, _dict_key, params)
return None
del _dict[_dict_key]
@ResourceHandler.register_resource_handler
@@ -493,14 +576,14 @@ class ResourceHandler_RestResourceBase(
self.resource.check_acl_self(self.req, None)
for key, attr in self.resource.model_fields.items():
if key in self.resource._plugins_:
if issubclass(self.resource._plugins_[key], ResourcePlugin_field):
plugin_field = cast(ResourcePlugin_field, self.resource._plugins_[key](self.req, self.req.get_root_resource()))
if isinstance(self.resource._plugins_[key], ResourcePlugin_field):
plugin_field = cast(ResourcePlugin_field, self.resource._plugins_[key])
plugin_field.set_context(self.req, self.req.get_root_resource())
value = getattr(self.resource, key)
setattr(self.resource, key, plugin_field.handle_field_get(value, params))
elif issubclass(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
plugin_resource = cast(
ResourcePlugin_RestResourceBase, self.resource._plugins_[key](self.req, self.req.get_root_resource())
)
elif isinstance(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
plugin_resource = cast(ResourcePlugin_RestResourceBase, self.resource._plugins_[key])
plugin_resource.set_context(self.req, self.req.get_root_resource())
value = getattr(self.resource, key)
setattr(self.resource, key, plugin_resource.handle_resource_get(value, params))
@@ -519,18 +602,14 @@ class ResourceHandler_RestResourceBase(
key = self.req.get_resource_origin(0)
if key in self.resource._plugins_:
if issubclass(self.resource._plugins_[key], ResourcePlugin_field):
plugin_field = cast(
ResourcePlugin_field,
self.resource._plugins_[key](self.req, self.req.get_root_resource()),
)
if isinstance(self.resource._plugins_[key], ResourcePlugin_field):
plugin_field = cast(ResourcePlugin_field, self.resource._plugins_[key])
plugin_field.set_context(self.req, self.req.get_root_resource())
value = plugin_field.handle_field_get(value, params)
elif issubclass(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
plugin_resource = cast(
ResourcePlugin_RestResourceBase,
self.resource._plugins_[key](self.req, self.req.get_root_resource()),
)
elif isinstance(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
plugin_resource = cast(ResourcePlugin_RestResourceBase, self.resource._plugins_[key])
plugin_resource.set_context(self.req, self.req.get_root_resource())
value = plugin_resource.handle_resource_get(value, params)
return value
@@ -539,50 +618,80 @@ class ResourceHandler_RestResourceBase(
# print(f"{type(self).__name__}->_process_put()")
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
assert self.prev_handler is not None
assert isinstance(self.resource, RestResourceBase)
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
# updating values based on new data
_new_resrc.update(**self.req.get_data())
# applying plugins (to nested element)
if isinstance(_new_resrc, RestResourceBase):
for key, attr in _new_resrc.model_fields.items():
if key in _new_resrc._plugins_:
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))
for key, _ 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])
plugin_field.set_context(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)
and self.prev_handler.prev_handler is not None
and isinstance(self.prev_handler.prev_handler.resource, RestResourceBase)
):
key = self.req.get_resource_origin(2)
if key in self.prev_handler.prev_handler.resource._plugins_:
plugin_dict: ResourcePlugin_dict = cast(
ResourcePlugin_dict,
self.prev_handler.prev_handler.resource._plugins_[key](self.req, self.req.get_root_resource()),
)
_new_resrc = plugin_dict.handle_dict_elem_put(_new_resrc, params)
# element is within a RestResourceBase
elif isinstance(self.prev_handler.resource, RestResourceBase):
key = self.req.get_resource_origin(1)
if key in self.prev_handler.resource._plugins_:
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
ResourcePlugin_RestResourceBase,
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__)
# element is within a dict
if (
isinstance(self.prev_handler.resource, dict)
and self.prev_handler.prev_handler is not None
and isinstance(self.prev_handler.prev_handler.resource, RestResourceBase)
):
key = self.req.get_resource_origin(2)
dict_key_type: T_T_DictKey = self.prev_handler.prev_handler.resource._dict_key_type_[self.req.get_resource_origin(2)]
_dict: dict[T_DictKey, T_DictValues] = cast(dict[T_DictKey, T_DictValues], self.prev_handler.resource)
_dict_key: T_DictKey
if key in self.prev_handler.prev_handler.resource._plugins_:
plugin_dict: ResourcePlugin_dict = cast(ResourcePlugin_dict, self.prev_handler.prev_handler.resource._plugins_[key])
plugin_dict.set_context(self.req, self.req.get_root_resource())
if issubclass(dict_key_type, bytes):
key_byte = dict_key_type(self.req.get_resource_origin(1), "utf-8")
_dict_key = key_byte
else:
key_std = dict_key_type(self.req.get_resource_origin(1))
_dict_key = key_std
plugin_dict.handle_dict_elem_put(_dict, _dict_key, _new_resrc, params)
else:
if issubclass(dict_key_type, bytes):
key_byte = dict_key_type(self.req.get_resource_origin(1), "utf-8")
_dict_key = key_byte
else:
key_std = dict_key_type(self.req.get_resource_origin(1))
_dict_key = key_std
if _dict_key not in _dict:
raise RuntimeError(f"Key not found: {str(_dict_key)}")
_dict[_dict_key] = _new_resrc
# element is within a RestResourceBase
elif isinstance(self.prev_handler.resource, RestResourceBase):
key = self.req.get_resource_origin(1)
if key in self.prev_handler.resource._plugins_:
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
ResourcePlugin_RestResourceBase, self.prev_handler.resource._plugins_[key]
)
plugin_rsrc.set_context(self.req, self.req.get_root_resource())
_new_resrc = plugin_rsrc.handle_resource_put(_new_resrc, params)
else:
self.resource.update(**_new_resrc.__dict__)
else:
raise RuntimeError("unsupported operation")
# print("***************")
# print(self.resource)
# print(_new_resrc)
# print(_new_resrc.__dict__)
return
def _handle_process_delete(self, params) -> None:
@@ -628,9 +737,9 @@ class ResourceHandler_simple(
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.req, self.req.get_root_resource()),
ResourcePlugin_field, self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)]
)
plugin_simple.set_context(self.req, self.req.get_root_resource())
return plugin_simple.handle_field_get(self.resource, params)
return self.resource
@@ -649,9 +758,9 @@ class ResourceHandler_simple(
if self.req.get_resource_origin(1) in self.prev_handler.resource._plugins_:
# print("PLUGIN FOUND")
plugin_simple: ResourcePlugin_field = cast(
ResourcePlugin_field,
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)](self.req, self.req.get_root_resource()),
ResourcePlugin_field, self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)]
)
plugin_simple.set_context(self.req, self.req.get_root_resource())
# print(value)
value = plugin_simple.handle_field_put(value, params)
# print(value)

View File

@@ -1,83 +0,0 @@
#!/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 TYPE_CHECKING
from .rest_resource_walker import (
RestResourceWalkerFutureResult,
RestResourceWalker_Root,
RestResourceWalker_Sub_T_Dict,
RestResourceWalker_Sub_RestFields,
RestResourceWalker_Sub_RestResourceBase,
)
if TYPE_CHECKING is True:
from typing import Optional, Any
class RestResourceWalkerFutureResult_RestResourceBase_handler(RestResourceWalkerFutureResult[dict]):
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
# print(f"RestResourceWalkerFutureResult_RestResourceBase_handler {result}")
res: dict[str, Any] = {}
res[self.source.resource_name] = {}
if result:
for subres in result:
key = next(iter(subres))
print(key)
res[self.source.resource_name] = res[self.source.resource_name] | subres
return res
class RestResourceWalkerFutureResult_Dict_handler(RestResourceWalkerFutureResult[dict]):
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
# print(f"RestResourceWalkerFutureResult_Dict_handler {result}")
res: dict[str, Any] = {}
if result:
for subres in result:
res = res | subres
return res
class RestResourceWalkerFutureResult_RestFields_handler(RestResourceWalkerFutureResult[dict]):
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
# print(f"RestResourceWalkerFutureResult_RestFields_handler {result}")
# print(self.source.resource)
res: dict[str, Any] = {}
res[self.source.resource_name] = {}
if result:
for subres in result:
key = next(iter(subres))
print(key)
res[self.source.resource_name] = res[self.source.resource_name] | subres
return res
class RestResourceWalker_Sub_T_Dict__handler(RestResourceWalker_Sub_T_Dict):
cls_RestResourceWalkerFutureResult = RestResourceWalkerFutureResult_Dict_handler
class RestResourceWalker_Sub_RestResourceBase__handler(RestResourceWalker_Sub_RestResourceBase):
cls_RestResourceWalkerFutureResult = RestResourceWalkerFutureResult_RestResourceBase_handler
class RestResourceWalker_Sub_RestResourceFields__handler(RestResourceWalker_Sub_RestFields):
cls_RestResourceWalkerFutureResult = RestResourceWalkerFutureResult_RestFields_handler
class RestResourceWalker_Root__handler(RestResourceWalker_Root):
cls_RestResourceWalker_Sub = [
RestResourceWalker_Sub_T_Dict__handler,
RestResourceWalker_Sub_RestResourceFields__handler,
RestResourceWalker_Sub_RestResourceBase__handler,
]

View File

@@ -13,7 +13,7 @@ from .rest_types import (
from .rest_exceptions import RestResourceConfigException
if TYPE_CHECKING is True:
if TYPE_CHECKING:
from .rest_request import RestRequest
from .rest_resource import RestResourceBase
from .rest_request_opt import (
@@ -30,7 +30,10 @@ if TYPE_CHECKING is True:
class ResourcePlugin(ABC):
def __init__(self, request: RestRequest, root_resource: RestResourceBase) -> None:
def __init__(self) -> None:
...
def set_context(self, request: RestRequest, root_resource: RestResourceBase) -> None:
self.__request: RestRequest = request
self.__root_resource: RestResourceBase = root_resource
@@ -130,6 +133,7 @@ class ResourcePlugin_dict(ResourcePlugin, Generic[_T_DictKey, _T_DictValues]):
def handle_dict_post(
self,
resource_dict: dict[_T_DictKey, _T_DictValues],
key: _T_DictKey,
resource: _T_DictValues,
params: RestRequestParams_Dict_POST[_T_DictKey],
) -> Optional[_T_DictKey]:
@@ -137,6 +141,15 @@ class ResourcePlugin_dict(ResourcePlugin, Generic[_T_DictKey, _T_DictValues]):
@abstractmethod
def handle_dict_delete(
self,
resource_dict: dict[_T_DictKey, _T_DictValues],
key: _T_DictKey,
params: RestRequestParams_Dict_DELETE[_T_DictKey],
) -> None:
...
@abstractmethod
def handle_dict_delete_all(
self,
resource_dict: dict[_T_DictKey, _T_DictValues],
params: RestRequestParams_Dict_DELETE[_T_DictKey],
@@ -146,17 +159,20 @@ class ResourcePlugin_dict(ResourcePlugin, Generic[_T_DictKey, _T_DictValues]):
@abstractmethod
def handle_dict_elem_get(
self,
resource: TV_RestResourceBase,
resource_dict: dict[_T_DictKey, _T_DictValues],
key: _T_DictKey,
params: RestRequestParams_Dict_elem_GET,
) -> TV_RestResourceBase:
) -> _T_DictValues:
...
@abstractmethod
def handle_dict_elem_put(
self,
resource: TV_RestResourceBase,
resource_dict: dict[_T_DictKey, _T_DictValues],
key: _T_DictKey,
resource: _T_DictValues,
params: RestRequestParams_Dict_elem_PUT,
) -> TV_RestResourceBase:
) -> None:
...
@@ -173,31 +189,43 @@ class ResourcePlugin_dict_default(ResourcePlugin_dict[_T_DictKey, _T_DictValues]
def handle_dict_post(
self,
resource_dict: dict[_T_DictKey, _T_DictValues],
key: _T_DictKey,
resource: _T_DictValues,
params: RestRequestParams_Dict_POST[_T_DictKey],
) -> Optional[_T_DictKey]:
if params.API_key is not None:
resource_dict[params.API_key] = resource
return params.API_key
resource_dict[key] = resource
return key
def handle_dict_delete(
self,
resource_dict: dict[_T_DictKey, _T_DictValues],
key: _T_DictKey,
params: RestRequestParams_Dict_DELETE[_T_DictKey],
) -> None:
if params.API_key is not None:
del resource_dict[params.API_key]
del resource_dict[key]
def handle_dict_delete_all(
self,
resource_dict: dict[_T_DictKey, _T_DictValues],
params: RestRequestParams_Dict_DELETE[_T_DictKey],
) -> None:
resource_dict.clear()
def handle_dict_elem_get(
self,
resource: TV_RestResourceBase,
resource_dict: dict[_T_DictKey, _T_DictValues],
key: _T_DictKey,
params: RestRequestParams_Dict_elem_GET,
) -> TV_RestResourceBase:
return resource
) -> _T_DictValues:
return resource_dict[key]
def handle_dict_elem_put(
self,
resource: TV_RestResourceBase,
resource_dict: dict[_T_DictKey, _T_DictValues],
key: _T_DictKey,
resource: _T_DictValues,
params: RestRequestParams_Dict_elem_PUT,
) -> TV_RestResourceBase:
return resource
) -> None:
if key not in resource_dict:
raise RuntimeError(f"Key not found: {str(key)}")
resource_dict[key] = resource

View File

@@ -6,7 +6,6 @@ from typing import (
TYPE_CHECKING,
)
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from .rest_resource import RestResourceBase
@@ -29,8 +28,8 @@ from .rest_ACL import (
)
from .rest_exceptions import RestResourcePluginException_InvalidPluginSignature, RestResourceModelException, RestResourceModelException_ACL
if TYPE_CHECKING is True:
pass
if TYPE_CHECKING:
...
class RestResourceWalker_Sub_T_Dict__tree_init(RestResourceWalker_Sub_T_Dict):
@@ -68,7 +67,7 @@ class RestResourceWalker_Sub_T_Dict__tree_init(RestResourceWalker_Sub_T_Dict):
plugin_dict: type[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
self.parent.annotation._plugins_[self.resource_name] = plugin_dict()
# print("ADD DICT PLUGIN")
if "ACL" in self.resource.json_schema_extra:
@@ -115,7 +114,7 @@ class RestResourceWalker_Sub_RestFields__tree_init(RestResourceWalker_Sub_RestFi
plugin_field: type[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
self.parent.annotation._plugins_[self.resource_name] = plugin_field()
# print("ADD FIELD PLUGIN")
if "ACL" in self.resource.json_schema_extra:
@@ -158,7 +157,7 @@ class RestResourceWalker_Sub_RestResourceBase__tree_init(RestResourceWalker_Sub_
plugin_resource: type[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
self.parent.annotation._plugins_[self.resource_name] = plugin_resource()
# print("ADD RESOURCE PLUGIN")
if "ACL" in self.resource.json_schema_extra:

View File

@@ -5,7 +5,6 @@ from typing import (
get_args,
get_origin,
TypeVar,
Type,
Generic,
TYPE_CHECKING,
)
@@ -17,7 +16,7 @@ from .rest_types import _T_SupportedRESTFields
from .rest_resource import RestResourceBase
from .rest_exceptions import RestResourceModelException
if TYPE_CHECKING is True:
if TYPE_CHECKING:
from typing import Any, Optional
TV_RestResourceWalkerFutureResult = TypeVar("TV_RestResourceWalkerFutureResult")
@@ -40,7 +39,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.
"""
@@ -51,7 +50,7 @@ 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]:
@@ -66,7 +65,7 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
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,
@@ -74,7 +73,7 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
):
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
@@ -124,11 +123,11 @@ 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
_anno: Type[Any]
_anno: type[Any]
# print("!!!!!!!!!!!!!!!!!!!!!!!")
# print(resource)
@@ -159,7 +158,7 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
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
@@ -175,7 +174,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
@@ -185,7 +184,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]:
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)),
@@ -201,15 +200,15 @@ class RestResourceWalker_Sub_RestResourceBase(RestResourceWalker_Sub):
class RestResourceWalker_Root:
cls_RestResourceWalker_Sub: list[Type[RestResourceWalker_Sub]] = [
cls_RestResourceWalker_Sub: list[type[RestResourceWalker_Sub]] = [
RestResourceWalker_Sub_T_Dict,
RestResourceWalker_Sub_RestFields,
RestResourceWalker_Sub_RestResourceBase,
]
def __init__(self, resource: RestResourceBase | Type[RestResourceBase]) -> None:
def __init__(self, resource: RestResourceBase | type[RestResourceBase]) -> None:
self.subwalker_argument: Any = None
self.resource: Type[RestResourceBase]
self.resource: type[RestResourceBase]
if isinstance(resource, RestResourceBase):
self.resource = type(resource)
else:
@@ -225,7 +224,7 @@ class RestResourceWalker_Root:
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()
]

View File

@@ -8,7 +8,7 @@ from pathlib import Path
from uuid import UUID
from ipaddress import IPv4Address, IPv4Network
if TYPE_CHECKING is True:
if TYPE_CHECKING:
from .rest_resource import RestResourceBase
T_Gen_DictKeys: type = type({}.keys())
@@ -91,7 +91,7 @@ _T_DictValues = TypeVar(
NoneType,
)
T_T_FieldValue = type(T_FieldValue)
T_T_FieldValue = type[T_FieldValue]
T_T_DictValues = type[T_DictValues]
T_Dict = dict[T_DictKey, T_DictValues]

View File

@@ -4,12 +4,12 @@ from os import chdir
from pathlib import Path
from typing import Optional, ClassVar
from time import sleep
import uvicorn
import socket
import requests
from contextlib import closing
from multiprocessing import Process
import requests
from requests.adapters import HTTPAdapter
import uvicorn
from src.pyrestresource import (
RestField,

View File

@@ -8,10 +8,6 @@ from uuid import UUID, uuid4
from time import time
import json
print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
RestResourceHandlerException_Forbiden,

View File

@@ -0,0 +1,193 @@
from __future__ import annotations
import unittest
from os import chdir
from pathlib import Path
from typing import Optional
from src.pyrestresource import (
RestField,
register_rest_rootpoint,
RestResourceBase,
rsrc_verb,
RestRequestParams_GET,
RestRequestParams_POST,
RestRequestParams_Dict_GET,
RestRequestParams_PUT,
RestRequestParams_Dict_elem_GET,
RestRequestParams_Dict_elem_PUT,
RestRequestParams_Dict_DELETE,
RestRequestParams_Dict_POST,
T_SupportedRESTFields,
ResourcePlugin_dict_default,
)
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 Test_Record(RestResourceBase):
test_str: str
test_int: int
class ResourcePlugin_dict_Test_Record(ResourcePlugin_dict_default[str, Test_Record]):
static_Test_Record_active: bool = True
static_Test_Record = Test_Record(test_str="mytest", test_int=84)
def handle_dict_get_keys(
self,
resource_dict: dict[str, Test_Record],
params: RestRequestParams_Dict_GET,
) -> list[str]:
result = super().handle_dict_get_keys(resource_dict, params)
if self.static_Test_Record_active is True:
result.append("static_elem")
return result
def handle_dict_elem_get(
self,
resource_dict: dict[str, Test_Record],
key: str,
params: RestRequestParams_Dict_elem_GET,
) -> Test_Record:
if key == "static_elem":
if self.static_Test_Record_active is True:
return self.static_Test_Record
else:
raise RuntimeError("Key Not Found")
return super().handle_dict_elem_get(resource_dict, key, params)
def handle_dict_delete(
self,
resource_dict: dict[str, Test_Record],
key: str,
params: RestRequestParams_Dict_DELETE[str],
) -> None:
if key == "static_elem":
self.static_Test_Record_active = False
else:
super().handle_dict_delete(resource_dict, key, params)
def handle_dict_delete_all(
self,
resource_dict: dict[str, Test_Record],
params: RestRequestParams_Dict_DELETE[str],
) -> None:
self.static_Test_Record_active = False
super().handle_dict_delete_all(resource_dict, params)
def handle_dict_elem_put(
self,
resource_dict: dict[str, Test_Record],
key: str,
resource: Test_Record,
params: RestRequestParams_Dict_elem_PUT,
) -> None:
if key == "static_elem":
if self.static_Test_Record_active is True:
self.static_Test_Record = resource
else:
raise RuntimeError("Key Not Found")
else:
super().handle_dict_elem_put(resource_dict, key, resource, params)
def handle_dict_post(
self,
resource_dict: dict[str, Test_Record],
key: str,
resource: Test_Record,
params: RestRequestParams_Dict_POST[str],
) -> Optional[str]:
resource.test_int = resource.test_int + 1
return super().handle_dict_post(resource_dict, key, resource, params)
@register_rest_rootpoint
class RootApp(RestResourceBase):
str_dict_Test_Record: dict[str, Test_Record] = RestField(
default={"test": Test_Record(test_str="Hi", test_int=42)},
plugin=ResourcePlugin_dict_Test_Record,
)
# 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()[Test_Record.__name__] = Test_Record
globals()[RootApp.__name__] = RootApp
class Test_RestAPI_Plugin_Dict(unittest.TestCase):
def setUp(self) -> None:
chdir(testdir_path.parent.resolve())
init_classes()
self.testapp = RootApp()
def test_get_root(self):
result = self.testapp.process_request("/", rsrc_verb.GET)
self.assertEqual(result.get_result(), "{}")
# print(result.get_result())
def test_get_dict_keys(self):
result = self.testapp.process_request("/str_dict_Test_Record", rsrc_verb.GET)
self.assertEqual(result.get_result(), '["test", "static_elem"]')
def test_get_dict_elems(self):
result = self.testapp.process_request("/str_dict_Test_Record/test", rsrc_verb.GET)
self.assertEqual(result.get_result(), '{"test_str": "Hi", "test_int": 42}')
result = self.testapp.process_request("/str_dict_Test_Record/static_elem", rsrc_verb.GET)
self.assertEqual(result.get_result(), '{"test_str": "mytest", "test_int": 84}')
def test_delete_dict_elems(self):
self.testapp.process_request("/str_dict_Test_Record/test", rsrc_verb.DELETE)
result = self.testapp.process_request("/str_dict_Test_Record", rsrc_verb.GET)
self.assertEqual(result.get_result(), '["static_elem"]')
self.testapp.process_request("/str_dict_Test_Record/static_elem", rsrc_verb.DELETE)
result = self.testapp.process_request("/str_dict_Test_Record", rsrc_verb.GET)
self.assertEqual(result.get_result(), "[]")
def test_delete_all_dict_elems(self):
self.testapp.process_request("/str_dict_Test_Record/test", rsrc_verb.DELETE)
result = self.testapp.process_request("/str_dict_Test_Record", rsrc_verb.GET)
self.assertEqual(result.get_result(), '["static_elem"]')
self.testapp.process_request("/str_dict_Test_Record", rsrc_verb.DELETE)
result = self.testapp.process_request("/str_dict_Test_Record", rsrc_verb.GET)
self.assertEqual(result.get_result(), "[]")
def test_delete_dict_elems_API_key(self):
self.testapp.process_request("/str_dict_Test_Record/test", rsrc_verb.DELETE)
result = self.testapp.process_request("/str_dict_Test_Record", rsrc_verb.GET)
self.assertEqual(result.get_result(), '["static_elem"]')
self.testapp.process_request("/str_dict_Test_Record?API_key=static_elem", rsrc_verb.DELETE)
result = self.testapp.process_request("/str_dict_Test_Record", rsrc_verb.GET)
self.assertEqual(result.get_result(), "[]")
def test_put_dict_elem(self):
self.testapp.process_request("/str_dict_Test_Record/test", rsrc_verb.PUT, '{"test_str": "Hi", "test_int": 43}')
result = self.testapp.process_request("/str_dict_Test_Record/test", rsrc_verb.GET)
self.assertEqual(result.get_result(), '{"test_str": "Hi", "test_int": 43}')
self.testapp.process_request("/str_dict_Test_Record/static_elem", rsrc_verb.PUT, '{"test_str": "Hi you", "test_int": 7}')
result = self.testapp.process_request("/str_dict_Test_Record/static_elem", rsrc_verb.GET)
self.assertEqual(result.get_result(), '{"test_str": "Hi you", "test_int": 7}')
def test_post_dict_elem(self):
result = self.testapp.process_request("/str_dict_Test_Record?API_key=newval", rsrc_verb.POST, '{"test_str": "Hi2", "test_int": 77}')
self.assertEqual(result.get_result(), '"newval"')
result = self.testapp.process_request("/str_dict_Test_Record/newval", rsrc_verb.GET)
self.assertEqual(result.get_result(), '{"test_str": "Hi2", "test_int": 78}')
result = self.testapp.process_request("/str_dict_Test_Record", rsrc_verb.GET)
self.assertEqual(result.get_result(), '["test", "newval", "static_elem"]')

View File

@@ -1,16 +1,11 @@
from __future__ import annotations
import unittest
from typing import Optional
from os import chdir
from pathlib import Path
from io import StringIO
from contextlib import redirect_stdout
print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
RestResourceBase,

View File

@@ -1,15 +1,9 @@
from __future__ import annotations
import unittest
from typing import Optional
from os import chdir
from pathlib import Path
print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
RestResourceBase,

View File

@@ -1,22 +1,16 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from os import chdir
from pathlib import Path
from typing import Optional
from uuid import UUID, uuid4
from time import time, sleep
import json
import uvicorn
import socket
import requests
from contextlib import closing
from multiprocessing import Process
from requests.adapters import HTTPAdapter
import coverage
print(__name__)
print(__package__)
import requests
from requests.adapters import HTTPAdapter
import uvicorn
from src.pyrestresource import (
RestField,
@@ -28,7 +22,6 @@ from src.pyrestresource import (
RestRequestParams_Dict_GET,
T_SupportedRESTFields,
)
from pprint import pprint
from test import ThreadedUvicorn