22 Commits

Author SHA1 Message Date
7625461929 Update Jenkinsfile 2024-10-12 16:43:41 +02:00
cclecle
8ba0ab5f5e chore: wip 2024-04-12 22:56:27 +01:00
cclecle
c05b541148 improve code quality 2023-11-11 17:17:35 +00:00
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
cclecle
6311d90a2d update jenkins & toml from project template 2023-11-06 14:56:46 +00:00
cclecle
7e13d49feb remove unused data dir from toml 2023-11-06 13:57:00 +00:00
cclecle
9b3e847908 use threaded uvicorn during test 2023-11-06 13:50:34 +00:00
cclecle
3afebdba33 temporary disable PERF tests 2023-11-06 11:25:49 +00:00
cclecle
0ec875e497 add parallel coverage option 2023-11-06 10:35:05 +00:00
cclecle
d0e146ac76 try to fix toml / coverage 2023-11-06 09:33:55 +00:00
cclecle
4af812cf80 enable multiprocessing on coverage 2023-11-06 09:27:53 +00:00
cclecle
d58173f07b fix typing issues 2023-11-06 01:11:43 +00:00
cclecle
04ef407a6f typo fix 2023-11-05 22:21:07 +00:00
cclecle
4dc7243900 add missing typegard dep 2023-11-05 22:17:33 +00:00
cclecle
4cc5080838 add a feature to keep exception on when called from python / but not
when called from uvicorn.
2023-11-05 22:11:21 +00:00
cclecle
cffa209c9a finish 1st login version + clean code + fix unittest regressions 2023-11-05 17:44:15 +00:00
cclecle
f00cf7b4b2 continuing implementation of login and session 2023-11-05 15:38:08 +00:00
cclecle
346ff649ec fix ACL + cleaning 2023-11-03 17:42:34 +00:00
33 changed files with 2365 additions and 1065 deletions

View File

@@ -1,6 +1,6 @@
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

18
Jenkinsfile vendored
View File

@@ -184,7 +184,7 @@ pipeline {
sh("virtualenv --pip=embed --setuptools=embed --wheel=embed --no-periodic-update --activators bash,python TOOLS_ENV")
sh(". ~/BUILD_ENV/bin/activate && pip install --upgrade setuptools build pip")
sh(". ~/BUILD_ENV/bin/activate && pip install --upgrade 'copier==8.*' jinja2-slug toml")
sh(". ~/BUILD_ENV/bin/activate && pip install --upgrade 'copier==9.*' jinja2-slug toml")
sh(". ~/TEST_ENV/bin/activate && pip install --upgrade pip")
@@ -426,9 +426,17 @@ pipeline {
}
post {
always {
dir("gitrepo") {
publishCoverage adapters: [cobertura(mergeToOneReport: true, path: "helpers-results/cl_types_check/cobertura.xml")]
junit 'helpers-results/cl_types_check/junit.xml'
dir("gitrepo") {
//publish coverage
recordCoverage( sourceDirectories: [[path: 'src']],
tools: [[parser: 'COBERTURA', pattern: 'helpers-results/cl_types_check/cobertura.xml']],
id: 'COBERTURA', name: 'COBERTURA Coverage',
sourceCodeRetention: 'EVERY_BUILD',)
//add type check to junit result set
junit 'helpers-results/cl_types_check/junit.xml'
//publish html reports files
publishHTML([
reportDir: "helpers-results/cl_quality_check",
reportFiles: "report.html",
@@ -538,7 +546,7 @@ pipeline {
dir("gitrepo") {
junit 'helpers-results/cl_unit_test/*.xml'
// using cobertura format (= coverage xml format)
publishCoverage adapters: [cobertura(mergeToOneReport: true, path: "helpers-results/cl_unit_test_coverage/test_coverage.xml")]
recordCoverage(tools: [[parser: 'COBERTURA', pattern: 'helpers-results/cl_unit_test_coverage/test_coverage.xml']])
publishHTML([
reportDir: "helpers-results/cl_unit_test_coverage",
reportFiles: "index.html",

View File

@@ -12,21 +12,23 @@
A RESTful API library built on top of pydantic & uvicorn to make service API from a data model.
/!\ early in-progress project for internal use ATM.
/!\\ early in-progress project for internal use ATM.
Feel free to contribute.
Features:
- use annotation
Features (available):
- type annotation used
- support containers (dict)
- support plugins (for hook and biding)
- user authentification (WIP)
- ACL (WIP)
- python internal model instance (with possible serialization/auto-save on-disk)
- user auth
- ACL
- daemon mode
Features(planned):
- group support
- python internal model instance (with possible serialization/auto-save on-disk)
Limitations:
- no nested reads / writes
- weak unitest (atm)
Checkout [Latest Documentation](https://chacha.ddns.net/mkdocs-web/chacha/pyrestresource/master/latest/).

View File

@@ -10,7 +10,7 @@
<stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:pyrestresource/helpers_proxy}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value=""/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="--typecheck --qualitycheck"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="--qualitycheck"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="pyrestresource"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>

17
RUN_types.launch Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/pyrestresource/helpers_proxy"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:pyrestresource/helpers_proxy}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value=""/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="--typecheck"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="pyrestresource"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

View File

@@ -34,6 +34,7 @@ classifiers = [
]
dependencies = [
'packaging',
'typeguard',
'pydantic>=2.4,<3',
'uvicorn>=0.23'
]
@@ -47,21 +48,42 @@ include-package-data = true
where = ["src"]
[tool.setuptools.package-data]
"pyrestresource.data" = ["*.*"]
"pyrestresource" = ["py.typed"]
[tool.pylint.main]
disable = ["missing-class-docstring","missing-function-docstring","missing-module-docstring"]
# [[tool.mypy.overrides]]
# module = ""
# ignore_missing_imports = true
[tool.coverage.run]
cover_pylib = false
branch = true
data_file="helpers-results/cl_unit_test_raw_coverage/.coverage"
# debug = ["config","multiproc","process"]
parallel = true
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/"
Tracker = "https://chacha.ddns.net/gitea/chacha/pyrestresource/issues"
[project.optional-dependencies]
test = ["chacha_cicd_helper@git+https://chacha.ddns.net/gitea/chacha/chacha_cicd_helper.git@master"]
coverage-check = ["chacha_cicd_helper@git+https://chacha.ddns.net/gitea/chacha/chacha_cicd_helper.git@master"]
complexity-check = ["chacha_cicd_helper@git+https://chacha.ddns.net/gitea/chacha/chacha_cicd_helper.git@master"]
quality-check = ["chacha_cicd_helper@git+https://chacha.ddns.net/gitea/chacha/chacha_cicd_helper.git@master"]
type-check = ["chacha_cicd_helper@git+https://chacha.ddns.net/gitea/chacha/chacha_cicd_helper.git@master"]
doc-gen = ["chacha_cicd_helper@git+https://chacha.ddns.net/gitea/chacha/chacha_cicd_helper.git@master"]
test = ["chacha_cicd_helper"]
coverage-check = ["chacha_cicd_helper"]
complexity-check = ["chacha_cicd_helper"]
quality-check = ["chacha_cicd_helper"]
type-check = ["chacha_cicd_helper"]
doc-gen = ["chacha_cicd_helper"]
# [project.scripts]
# my-script = "my_package.module:function"

View File

@@ -18,24 +18,9 @@ from typing import TYPE_CHECKING
from .__metadata__ import __version__, __Summuary__, __Name__
from .rest_resource import (
register_rest_rootpoint,
RestResourceBase,
)
from .rest_model import RestField
from .rest_resource_rootpoint import register_rest_rootpoint
from .rest_types import rsrc_verb, T_SupportedRESTFields
if TYPE_CHECKING:
from .rest_types import (
T_ListIndex,
T_ListSize,
T_DictKey,
T_T_DictKey,
T_DictValues,
T_T_DictValues,
)
from .rest_request_opt import (
RestRequestParams_POST,
RestRequestParams_DELETE,
@@ -46,11 +31,36 @@ 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,
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_resource import RestResourceBase
from .rest_login import UserLogin
from .rest_resource_login import RestResourceBaseLogin
from .rest_exceptions import (
RestResourceException,
RestResourceLoginException,
RestResourceLoginException_SessionTimeout,
RestResourceLoginException_ClientChange,
RestResourceLoginException_InvalidSession,
RestResourcePluginException,
RestResourcePluginException_InvalidPluginSignature,
RestResourceHandlerException_Forbiden,
)
if TYPE_CHECKING:
from .rest_types import (
T_ListIndex,
T_ListSize,
T_DictKey,
T_T_DictKey,
T_DictValues,
T_T_DictValues,
)

View File

@@ -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,23 @@ 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 | None]:
result: dict[str, str | None] = {}
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
traceback.print_exc()

View File

@@ -1,10 +1,15 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pydantic import BaseModel
from enum import Enum, auto
from pydantic import BaseModel
from .rest_types import rsrc_verb
if TYPE_CHECKING:
from .rest_login import UserLogin
class ACL_target(BaseModel):
pass
@@ -13,8 +18,12 @@ class ACL_target(BaseModel):
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):
class ACL_target_user_Annonymous(ACL_target_user):
name: str = "__ANNONYMOUS__"
@@ -22,10 +31,6 @@ class ACL_target_group(ACL_target):
name: str
class ACL_target_group_Annonymous(ACL_target):
name: str = "__ANNONYMOUS__"
class ACL_target_group_Any(ACL_target_group):
name: str = "__ANY__"

View File

@@ -0,0 +1,62 @@
class RestResourceException(Exception):
pass
class RestResourceConfigException(RestResourceException):
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

View File

@@ -0,0 +1,51 @@
#!/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, TYPE_CHECKING
from datetime import datetime
from pydantic import BaseModel
from .rest_types import rsrc_verb
# from .rest_resource import RestResourceBase
from . import rest_resource
from .rest_model import RestField
from .rest_ACL import ACL_record, ACL_target_group_Any, ACL_rule
if TYPE_CHECKING:
pass
class UserLogin(BaseModel):
username: str
secret: str
class UserSession(BaseModel):
last_update: datetime
user_login: UserLogin
client: tuple[str, int] | tuple[()] | None
class Login(rest_resource.RestResourceBase):
username: Optional[str] = RestField(None)
secret: Optional[str] = RestField(
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),
],
)

View File

@@ -0,0 +1,97 @@
from __future__ import annotations
from typing import Literal, Any, Callable, TYPE_CHECKING
from pydantic.fields import Field, _Unset, PydanticUndefined
from .rest_exceptions import RestResourceModelException
if TYPE_CHECKING:
from .rest_ACL import ACL_record
from .rest_resource_plugin import ResourcePlugin
from typing import Unpack
from pydantic.fields import _EmptyKwargs, AliasPath, AliasChoices
def RestField( # pylint: disable=too-many-locals
default: Any = PydanticUndefined,
*,
default_factory: Callable[[], Any] | None = _Unset,
alias: str | None = _Unset,
alias_priority: int | 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,
exclude: bool | None = _Unset,
discriminator: str | None = _Unset,
json_schema_extra: dict[str, Any] | Callable[[dict[str, Any]], None] | None = _Unset,
frozen: bool | None = _Unset,
validate_default: bool | None = _Unset,
repr: bool = _Unset, # pylint: disable=redefined-builtin
init_var: bool | None = _Unset,
kw_only: bool | None = _Unset,
pattern: str | None = _Unset,
strict: bool | None = _Unset,
gt: float | None = _Unset,
ge: float | None = _Unset,
lt: float | None = _Unset,
le: float | None = _Unset,
multiple_of: float | None = _Unset,
allow_inf_nan: bool | None = _Unset,
max_digits: int | None = _Unset,
decimal_places: int | None = _Unset,
min_length: int | None = _Unset,
max_length: int | None = _Unset,
union_mode: Literal["smart", "left_to_right"] = _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:
if extra:
json_schema_extra = extra # type: ignore
else:
json_schema_extra = {}
if ACL is not _Unset:
json_schema_extra["ACL"] = ACL
if plugin is not _Unset:
json_schema_extra["plugin"] = plugin
else:
raise RestResourceModelException("json_schema_extra must not be set")
return Field(
default,
default_factory=default_factory,
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
serialization_alias=serialization_alias,
title=title,
description=description,
examples=examples,
exclude=exclude,
discriminator=discriminator,
json_schema_extra=json_schema_extra,
frozen=frozen,
validate_default=validate_default,
repr=repr,
init_var=init_var,
kw_only=kw_only,
pattern=pattern,
strict=strict,
gt=gt,
ge=ge,
lt=lt,
le=le,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
min_length=min_length,
max_length=max_length,
union_mode=union_mode,
**extra,
)

View File

@@ -3,16 +3,18 @@
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,8 +26,19 @@ 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
from .rest_exceptions import (
RestResourceHandlerException_MethodNotAllowed,
RestResourceHandlerException_BadRequest,
RestResourceException,
RestResourceConfigException,
)
from .rest_ACL import ACL_target_user, ACL_target_user_Annonymous, ACL_target_group, ACL_target_group_Annonymous
if TYPE_CHECKING:
from typing import Optional
from .rest_types import T_SupportedRESTFields
from .rest_resource import RestResourceBase
class RequestFactory(
@@ -44,7 +57,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:
@@ -55,14 +70,14 @@ 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, request: RestRequest) -> None:
"""create an updated copy of a RestRequest object based on a different LUT_verb configuration
@@ -80,23 +95,22 @@ class RequestFactory(
elif request.verb is rsrc_verb.DELETE:
request.update_ReqParams(self.cls_RestRequestParams_DELETE)
else:
raise RuntimeError("Invalid Verb")
return
raise RestResourceHandlerException_MethodNotAllowed("Invalid Verb")
class RestRequest(Generic[_T_RestRequestParams]):
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
"""Main RestRequets class"""
def __init__(
self,
type_request_params: type[_T_RestRequestParams],
root_resource: RestResourceBase,
url: str,
verb: rsrc_verb,
data: Optional[dict[str, T_SupportedRESTFields]] = None,
query_string: Optional[str] = None,
incoming_cookie: dict[str, str] = {},
outgoing_cookie: dict[str, str] = {},
) -> None:
"""class to handle a request context, that will be kept and updated while walking url parts
@@ -113,22 +127,30 @@ class RestRequest(Generic[_T_RestRequestParams]):
self.url: str
self.verb: rsrc_verb
self.data: dict
self._raw_headers: list[Any] = []
self._client: tuple[str, int] | tuple[()] = ()
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
self.incoming_cookie: dict[str, str] = incoming_cookie
self.outgoing_cookie: dict[str, str] = outgoing_cookie
self.outgoing_cookie: dict[str, str] = {}
self.user: ACL_target_user = ACL_target_user_Annonymous()
self.group: ACL_target_group = ACL_target_group_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 :)
@@ -146,6 +168,60 @@ 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] | tuple[()]:
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 self.headers["cookie"] is None:
return None
if key not in self.headers["cookie"]:
return None
if isinstance(self.headers["cookie"], dict):
return self.headers["cookie"][key]
return None
def set_resp_cookie_value(self, key: str, value: str) -> None:
from .rest_resource_login import RestResourceBaseLogin
if not isinstance(self.root_resource, RestResourceBaseLogin):
raise RestResourceConfigException("root_resource must be RestResourceBaseLogin to use user_login")
expire_date = self.root_resource.get_new_cookie_expiration_date().strftime("%a, %d %b %Y %H:%M:%S GMT")
self.outgoing_cookie[key] = f"{value}; expires={expire_date}; 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 | dict[str, str | None] | None:
return self.headers["host"]
def set_result(self, result: str):
self.result = result
@@ -153,10 +229,13 @@ class RestRequest(Generic[_T_RestRequestParams]):
return self.result
def set_user(self, user: ACL_target_user):
self.user: ACL_target_user = user
self.user = user
def set_group(self, group: ACL_target_group):
self.group: ACL_target_group = group
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)

View File

@@ -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:
pass
class RestRequestParams(BaseModel, extra=Extra.allow):
pass

View File

@@ -1,249 +1,56 @@
#!/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, Field
from pydantic import BaseModel
from .helpers import _JSONEncoder
from .rest_types import rsrc_verb, _T_SupportedRESTFields
from .rest_resource_plugin import (
ResourcePlugin_field,
ResourcePlugin_RestResourceBase,
ResourcePlugin_dict,
)
from .rest_types import rsrc_verb
from .helpers import _JSONEncoder, forward_exception
from .rest_ACL import (
ACL_record,
ACL_target_user,
ACL_target_group,
ACL_target_user_Annonymous,
ACL_target_group_Annonymous,
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_exceptions import (
RestResourceLoginException_InvalidSession,
RestResourceLoginException_SessionTimeout,
RestResourceLoginException_ClientChange,
RestResourceLoginException_InvalidCredentials,
RestResourceHandlerException_ResourceNotFound,
RestResourceHandlerException_MethodNotAllowed,
RestResourceHandlerException_BadRequest,
RestResourceHandlerException_Forbiden,
RestResourceException,
)
from .rest_request import RestRequest
if TYPE_CHECKING:
from .rest_request import RestRequest
from .rest_types import T_SupportedRESTFields
from .rest_resource_plugin import ResourcePlugin
from .rest_types import (
T_ListIndex,
T_ListSize,
T_DictKey,
T_T_DictKey,
T_DictValues,
T_T_DictValues,
T_SupportedRESTFields,
)
from .rest_resource_handler import (
ResourceHandler,
)
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
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 isinstance(plugin_dict, ResourcePlugin_dict):
raise RuntimeError("Wrong plugin signature provided")
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 RuntimeError("ACL must be a list()")
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):
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 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:
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")
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 RuntimeError("ACL must be a list()")
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_", {})
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 isinstance(plugin_resource, ResourcePlugin_RestResourceBase):
raise RuntimeError("Wrong plugin signature provided")
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 RuntimeError("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
class RestResourceBase(ABC, BaseModel, validate_assignment=True):
_resp_cookies: ClassVar[dict[str, str]] = dict()
_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]] = {}
@@ -251,55 +58,58 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
_plugins_: ClassVar[
dict[
str,
list[ACL_record],
ResourcePlugin,
]
] = {}
_ACL_record_: ClassVar[
dict[
str,
ACL_record,
list[ACL_record],
]
] = {}
def _check_acl(self, user: ACL_target_user, group: ACL_target_group, verb: rsrc_verb, field: str):
print(f"evaluate self ACLs rule: {self._ACL_record_}")
if verb is rsrc_verb.GET and self.model_fields[field].exclude is True:
print("ALLOWED (excluded field)")
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}")
# 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)")
# print("ALLOWED (user)")
return
raise RuntimeError(f"Not allowed access detected: {field}")
raise RestResourceHandlerException_Forbiden(f"Not allowed access detected: {field}")
elif isinstance(acl.target, ACL_target_group):
if group == acl.target or acl.target == ACL_target_group_Any():
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)")
# print("ALLOWED (group)")
return
raise RuntimeError(f"Not allowed access detected: {field}")
raise RestResourceHandlerException_Forbiden(f"Not allowed access detected: {field}")
else:
raise RuntimeError(f"Wrong ACL target type: {field}")
print("ALLOWED (Default)")
raise RestResourceException(f"Wrong ACL target type: {field}")
# print("ALLOWED (Default)")
def check_acl_access(self, request: RestRequest) -> None:
def check_acl_field(self, request: RestRequest, req_index: int = 0) -> None:
"""Check ACL on requested field access"""
self._check_acl(request.user, request.group, request.get_verb(), request.get_resource_origin(0))
self._check_acl(request.user, request.groups, request.get_verb(), request.get_resource_origin(req_index), False)
def check_acl_operation(self, request: RestRequest, new_data: Optional[dict[str, _T_SupportedRESTFields]]) -> None:
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.group, rsrc_verb.GET, key)
for key in self.model_fields:
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.group, rsrc_verb.PUT, key)
if new_data is not None:
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 RuntimeError("Incompatible verb")
raise RestResourceException("Incompatible verb")
def update(self, **new_data):
for field, value in new_data.items():
@@ -319,34 +129,35 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
return body
async def __call__(self, scope, receive, send):
async def __call__(self, scope, receive, send) -> None:
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"
# import pprint
# print("----REC HEADER ---")
# pprint.pprint(scope["headers"])
# pprint.pprint(scope)
body = await self.read_body(receive)
verb = rsrc_verb[scope["method"]]
request: RestRequest = self.process_request(
scope["path"], rsrc_verb[scope["method"]], body.decode("utf-8"), scope["query_string"].decode("utf-8")
scope["path"],
rsrc_verb[scope["method"]],
body.decode("utf-8"),
scope["query_string"].decode("utf-8"),
scope["client"],
scope["headers"],
True,
)
assert request != None
assert request is not None
status = 200
if verb in (rsrc_verb.POST, rsrc_verb.PUT):
status = 201
header_resp = {
header_resp: dict[str, Any] = {
"type": "http.response.start",
"status": status,
"status": request.get_status(),
"headers": [
[b"content-type", b"application/json"],
],
@@ -355,13 +166,12 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
for name, value in request.outgoing_cookie.items():
header_resp["headers"].append(["Set-Cookie", f"{name}={value}"])
# print("----SENT HEADER ---")
# pprint.pprint(header_resp)
await send(header_resp)
body = None
if request.get_result():
body = request.get_result().encode("utf-8")
result = request.get_result()
if result:
body = result.encode("utf-8")
await send(
{
@@ -370,36 +180,74 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
}
)
def process_request(
def _process_request_session(self, request: RestRequest) -> None:
pass
def process_request( # pylint: disable=too-complex
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,
)
from .rest_resource_handler import ResourceHandler_RestResourceBase
data: dict = {}
if data_json:
data = json.loads(data_json)
# creating the root handler
ressource_handler: ResourceHandler = ResourceHandler_RestResourceBase(self, url, verb, data, query_string)
# preparing request & session
request: RestRequest = ressource_handler.get_request()
assert request != None
assert request is not None
result = ressource_handler.process_verb()
if headers is not None:
request.set_headers(headers)
# print("OOO")
# print(type(self)._resp_cookies)
# print("OOO2")
if client is not None:
request.set_client(client)
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))
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

View File

@@ -1,33 +1,29 @@
# pylint: disable=protected-access
from __future__ import annotations
import abc
from typing import Optional, cast, TypeVar, Generic, Self, TYPE_CHECKING
import abc
from .rest_types import (
NoneType,
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_resource import RestResourceBase
from . import rest_resource
from .rest_request import RequestFactory
from .rest_resource_plugin import (
ResourcePlugin_field,
ResourcePlugin_dict,
ResourcePlugin_RestResourceBase,
)
from .rest_ACL import (
ACL_target_user,
ACL_target_group,
ACL_target_user_Annonymous,
ACL_target_group_Annonymous,
ACL_target_group_Any,
ACL_rule,
)
from .rest_request_opt import (
RestRequestParams_POST,
RestRequestParams_DELETE,
@@ -44,18 +40,19 @@ from .rest_request_opt import (
_T_RestRequestParams_PUT,
)
from .rest_resource_handler_walker import RestResourceWalker_Root__handler
from .rest_exceptions import (
RestResourceHandlerException,
RestResourceHandlerException_ResourceNotFound,
RestResourceHandlerException_MethodNotAllowed,
RestResourceHandlerException_BadRequest,
)
if TYPE_CHECKING:
from .rest_types import (
T_ListIndex,
T_ListSize,
T_T_DictKey,
T_FieldValue,
)
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)
_T_Resource = TypeVar("_T_Resource", T_DictValues, T_Dict, T_SupportedRESTFields, rest_resource.RestResourceBase)
class ResourceHandler(
@@ -108,13 +105,13 @@ class ResourceHandler(
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")
assert url is not None and verb is not None
assert isinstance(resource, rest_resource.RestResourceBase)
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()}")
@@ -133,7 +130,7 @@ 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:
@@ -151,14 +148,6 @@ class ResourceHandler(
resource_handler = self._find_resource()
return resource_handler._process_verb()
def access_resource(
self,
) -> _T_Resource:
# print(f"[TRACE] {type(self).__name__}->access_resource()")
self._reset_context()
resource_handler = self._find_resource()
return resource_handler.resource
def _reset_context(self) -> None:
self.req.reset_url_stack()
@@ -183,13 +172,9 @@ class ResourceHandler(
# reveal_type(next_resource)
_next_resource = cast(_T_Resource, next_resource)
# reveal_type(_next_resource)
print(f"[DEBUG] next_resource = {type(next_resource).__name__}")
# print(f"[DEBUG] next_resource = {type(next_resource).__name__}")
if (
isinstance(_next_resource, RestResourceBase)
or isinstance(_next_resource, dict)
or type(_next_resource) in _T_SupportedRESTFields
):
if isinstance(_next_resource, (rest_resource.RestResourceBase, dict)) or type(_next_resource) in _T_SupportedRESTFields:
next_resource_handler_cls: type[ResourceHandler] = self._get_resource_handler(_next_resource, self.req)
self.saved_url = self.req.consume_url_stack(self._nb_url_element_to_consume_)
@@ -204,7 +189,7 @@ class ResourceHandler(
return next_resource_handler
# in _find_resource context, only resource's real values can be retrieved
raise RuntimeError("Wrong request")
raise RestResourceHandlerException_ResourceNotFound()
def _check_access_rights(self):
pass
@@ -227,7 +212,7 @@ class ResourceHandler(
self._process_delete()
return None
raise RuntimeError("Invalid Verb")
raise RestResourceHandlerException_BadRequest("Invalid Verb")
def _process_get(
self,
@@ -248,16 +233,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
@@ -300,66 +285,129 @@ 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:
# 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)
if params.API_key is not None:
del _dict[dict_key_type(params.API_key)]
else:
_dict.clear()
return
def _handle_process_post(self, params) -> Optional[T_DictKey]:
# pylint: disable=protected-access
# 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")
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_[
dict_key_type: T_T_DictKey = cast(rest_resource.RestResourceBase, self.prev_handler.resource)._dict_key_type_[
self.req.get_resource_origin(1)
]
_obj = dict_value_type(**self.req.get_data())
_dict: dict[T_DictKey, T_DictValues] = cast(dict[T_DictKey, T_DictValues], self.resource)
_dict_key: T_DictKey
_dict: dict[T_DictKey, "T_DictValues"] = cast(dict[T_DictKey, "T_DictValues"], self.resource)
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:
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
del _dict[_dict_key]
else:
if plugin_dict:
plugin_dict.handle_dict_delete_all(_dict, params)
return
_dict.clear()
return
def _handle_process_post(self, params) -> Optional[T_DictKey]: # pylint: disable=too-complex,too-many-branches
# print(f"{type(self).__name__}->_handle_process_post()")
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
assert self.prev_handler is not None
dict_key_type: T_T_DictKey = cast(rest_resource.RestResourceBase, self.prev_handler.resource)._dict_key_type_[
self.req.get_resource_origin(1)
]
dict_value_type: T_T_DictValues = cast(rest_resource.RestResourceBase, self.prev_handler.resource)._dict_value_type_[
self.req.get_resource_origin(1)
]
_obj: T_DictValues
if issubclass(dict_value_type, rest_resource.RestResourceBase):
_obj = dict_value_type(**self.req.get_data())
_obj_restrsrc = cast(rest_resource.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 isinstance(_obj, rest_resource.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, rest_resource.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
RuntimeError("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)
_dict[_dict_key] = _obj
return _dict_key
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
@@ -398,17 +446,29 @@ 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
assert isinstance(self.prev_handler.resource, rest_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()")
@@ -418,23 +478,36 @@ 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
assert isinstance(self.prev_handler.resource, rest_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
del _dict[_dict_key]
return
@ResourceHandler.register_resource_handler
class ResourceHandler_RestResourceBase(
ResourceHandler[
RestResourceBase,
rest_resource.RestResourceBase,
_T_RestRequestParams_POST,
_T_RestRequestParams_DELETE,
RestRequestParams_RestResourceBase_GET,
@@ -460,135 +533,159 @@ class ResourceHandler_RestResourceBase(
def _check_resource_handler(cls, resource: _T_Resource, req: RestRequest) -> bool:
# print(f"{cls.__name__}->_check_resource_handler()")
return isinstance(resource, RestResourceBase)
return isinstance(resource, rest_resource.RestResourceBase)
def _check_access_rights(self) -> None:
super()._check_access_rights()
print(f"{type(self).__name__}->_check_access_rights()")
# print(f"{type(self).__name__}->_check_access_rights()")
if self.req.get_resource_origin(0) == "/":
return
print("==================")
print(self.req.get_resource_origin(0))
# 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 RuntimeError(f"Unknown field access detected: {self.req.get_url_stack()}")
raise RestResourceHandlerException_ResourceNotFound(f"Unknown field access detected: {self.req.get_url_stack()}")
self.resource.check_acl_access(self.req)
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 RuntimeError(f"Not allowed READ access detected: {self.req.get_url_stack()}")
""" # not sure init_var has the expected behavior (read_only)
if self.resource.model_fields[self.req.get_resource_origin(0)].init_var is True and self.req.get_verb() in [
rsrc_verb.POST,
rsrc_verb.PUT,
rsrc_verb.DELETE,
]:
raise RuntimeError(f"Not allowed WRITE access detected: {self.req.get_url_stack()}")
"""
raise RestResourceHandlerException_ResourceNotFound(f"Not allowed READ access detected: {self.req.get_url_stack()}")
def _handle_process_get(self, params) -> RestResourceBase:
def _handle_process_get(self, params) -> rest_resource.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 (operation)
# So we are in a RestResourceBase instance and must return the content
plugin_field: ResourcePlugin_field
plugin_resource: ResourcePlugin_RestResourceBase
if len(self.req.get_url_stack()) == 0:
self.resource.check_acl_operation(self.req)
for key, attr in self.resource.model_fields.items():
self.resource.check_acl_self(self.req, None)
for key, _ 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](self.req))
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 isinstance(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
plugin_field: ResourcePlugin_field = cast(ResourcePlugin_RestResourceBase, self.resource._plugins_[key](self.req))
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_field.handle_resource_get(value, params))
setattr(self.resource, key, plugin_resource.handle_resource_get(value, params))
# result = RestResourceWalker_Root__handler(self.resource).process()
# print(result)
return self.resource
# 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 (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):
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
ResourcePlugin_RestResourceBase,
self.resource._plugins_[key](self.req),
)
value = plugin_rsrc.handle_field_get(value, params)
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 isinstance(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
ResourcePlugin_RestResourceBase,
self.resource._plugins_[key](self.req),
)
value = plugin_rsrc.handle_resource_get(value, params)
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
def _handle_process_put(self, params) -> None:
def _handle_process_put(self, params) -> None: # pylint: disable=too-complex,too-many-branches
# print(f"{type(self).__name__}->_process_put()")
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
self.resource.check_acl_operation(self.req, self.req.get_data())
assert self.prev_handler is not None
assert isinstance(self.resource, rest_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 isinstance(_new_resrc._plugins_[key], ResourcePlugin_field):
plugin_field: ResourcePlugin_field = cast(ResourcePlugin_field, _new_resrc._plugins_[key](self.req))
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_rsrc: ResourcePlugin_RestResourceBase = cast(
ResourcePlugin_RestResourceBase,
self.prev_handler.prev_handler.resource._plugins_[key](self.req),
)
_new_resrc = plugin_rsrc.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),
)
_new_resrc = plugin_rsrc.handle_resource_put(_new_resrc, params)
self.resource.update(**_new_resrc.dict())
return
# 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, rest_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, rest_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__)
def _handle_process_delete(self, params) -> None:
# print(f"{type(self).__name__}->_handle_process_delete()")
@@ -599,11 +696,11 @@ class ResourceHandler_RestResourceBase(
self.prev_handler is not None
and isinstance(self.prev_handler.resource, dict)
and self.prev_handler.prev_handler is not None
and isinstance(self.prev_handler.prev_handler.resource, RestResourceBase)
and isinstance(self.prev_handler.prev_handler.resource, rest_resource.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
@@ -627,13 +724,15 @@ class ResourceHandler_simple(
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
assert self.prev_handler is not None
assert isinstance(self.prev_handler.resource, RestResourceBase)
assert isinstance(self.prev_handler.resource, rest_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.req),
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
@@ -643,16 +742,18 @@ class ResourceHandler_simple(
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
assert self.prev_handler is not None
assert isinstance(self.prev_handler.resource, RestResourceBase)
assert isinstance(self.prev_handler.resource, rest_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")
plugin_simple: ResourcePlugin_field = cast(
ResourcePlugin_field,
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)](self.req),
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,84 +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 (
ClassVar,
get_args,
get_origin,
Optional,
TYPE_CHECKING,
)
from .rest_resource_walker import (
RestResourceWalkerFutureResult,
RestResourceWalker_Root,
RestResourceWalker_Sub_T_Dict,
RestResourceWalker_Sub_RestFields,
RestResourceWalker_Sub_RestResourceBase,
)
class RestResourceWalkerFutureResult_RestResourceBase_handler(RestResourceWalkerFutureResult[dict]):
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
print(f"RestResourceWalkerFutureResult_RestResourceBase_handler {result}")
res = {}
res[self.source.resource_name] = dict()
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 = {}
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 = {}
res[self.source.resource_name] = dict()
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

@@ -0,0 +1,83 @@
from __future__ import annotations
from typing import ClassVar, TYPE_CHECKING
from secrets import token_hex, compare_digest
from datetime import datetime, timedelta
from .rest_resource import RestResourceBase
from .rest_model import RestField
from .rest_ACL import ACL_target_user
from .rest_resource_plugin_login import ResourcePlugin_Login
from .rest_login import UserSession, Login
from .rest_exceptions import (
RestResourceLoginException_InvalidCredentials,
RestResourceLoginException_ClientChange,
RestResourceLoginException_SessionTimeout,
RestResourceLoginException_InvalidSession,
)
if TYPE_CHECKING:
from .rest_request import RestRequest
from .rest_login import UserLogin
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 = RestField(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 is not 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()
# print(f"non-connected user {request.get_client()}")
return
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)
already_failed = True
else:
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
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

View File

@@ -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,10 +10,11 @@ from .rest_types import (
TV_SupportedRESTFields,
TV_RestResourceBase,
)
from .rest_exceptions import RestResourceConfigException
from .rest_request import RestRequest
if TYPE_CHECKING or True:
if TYPE_CHECKING:
from .rest_request import RestRequest
from .rest_resource import RestResourceBase
from .rest_request_opt import (
RestRequestParams_GET,
RestRequestParams_PUT,
@@ -26,21 +28,43 @@ if TYPE_CHECKING or True:
)
class ResourcePlugin(Protocol):
def __init__(self, request: RestRequest) -> None:
self.request: RestRequest = request
class ResourcePlugin(ABC):
def __init__(self) -> None:
self.__request: RestRequest
self.__root_resource: RestResourceBase
def set_resp_cookie(self, name: str, value: str):
# print("AAA")
# print(name)
# print(value)
# print(self.cookies)
# print(type(self.cookies))
self.request.outgoing_cookie[name] = value
def set_context(self, request: RestRequest, root_resource: RestResourceBase) -> None:
self.__request = request
self.__root_resource = root_resource
def user_login(self, user_name: str, user_secret: str) -> str:
from .rest_resource_login import RestResourceBaseLogin
if not isinstance(self.__root_resource, RestResourceBaseLogin):
raise RestResourceConfigException("root_resource must be RestResourceBaseLogin to use user_login")
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:
from .rest_resource_login import RestResourceBaseLogin
if not isinstance(self.__root_resource, RestResourceBaseLogin):
raise RestResourceConfigException("root_resource must be RestResourceBaseLogin to use get_new_cookie_expiration_date")
return self.__root_resource.get_new_cookie_expiration_date()
def set_resp_status(self, status: int) -> None:
self.__request.set_resp_status(status)
@runtime_checkable
class ResourcePlugin_field(ResourcePlugin, Protocol[TV_SupportedRESTFields]):
class ResourcePlugin_field(ResourcePlugin, Generic[TV_SupportedRESTFields]):
@abstractmethod
def handle_field_get(self, resource: TV_SupportedRESTFields, params: RestRequestParams_GET) -> TV_SupportedRESTFields:
...
@@ -60,8 +84,7 @@ class ResourcePlugin_field_default(ResourcePlugin_field[TV_SupportedRESTFields])
return resource
@runtime_checkable
class ResourcePlugin_RestResourceBase(ResourcePlugin, Protocol[TV_RestResourceBase]):
class ResourcePlugin_RestResourceBase(ResourcePlugin, Generic[TV_RestResourceBase]):
@abstractmethod
def handle_resource_get(
self,
@@ -97,8 +120,7 @@ class ResourcePlugin_RestResourceBase_default(ResourcePlugin_RestResourceBase[TV
return resource
@runtime_checkable
class ResourcePlugin_dict(ResourcePlugin, Protocol[_T_DictKey, _T_DictValues]):
class ResourcePlugin_dict(ResourcePlugin, Generic[_T_DictKey, _T_DictValues]):
@abstractmethod
def handle_dict_get_keys(
self,
@@ -111,6 +133,7 @@ class ResourcePlugin_dict(ResourcePlugin, Protocol[_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]:
@@ -118,6 +141,15 @@ class ResourcePlugin_dict(ResourcePlugin, Protocol[_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],
@@ -127,17 +159,20 @@ class ResourcePlugin_dict(ResourcePlugin, Protocol[_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:
...
@@ -154,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

@@ -0,0 +1,24 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .rest_resource_plugin import ResourcePlugin_RestResourceBase_default
from .rest_login import UserLogin, Login
from .rest_exceptions import RestResourceLoginException_InvalidCredentials
if TYPE_CHECKING:
from .rest_request_opt import RestRequestParams_RestResourceBase_PUT, RestRequestParams_RestResourceBase_GET
class ResourcePlugin_Login(ResourcePlugin_RestResourceBase_default):
ar_UserLogin: list[UserLogin] = []
def handle_resource_get(self, resource: Login, params: RestRequestParams_RestResourceBase_GET) -> Login:
return Login(username=self.get_user_login(), secret=None)
def handle_resource_put(self, resource: Login, params: RestRequestParams_RestResourceBase_PUT) -> Login:
if resource.username is None or resource.secret is None:
raise RestResourceLoginException_InvalidCredentials()
token = self.user_login(resource.username, resource.secret)
self.set_resp_cookie_value("Authorization", f"Bearer {token}")
return resource

View File

@@ -0,0 +1,183 @@
# pylint: disable=protected-access
from __future__ import annotations
from typing import (
cast,
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:
...
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("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
assert isinstance(self.resource, FieldInfo)
current_resource = cast(FieldInfo, self.resource)
current_resource.exclude = True
parent_resource = cast(type[RestResourceBase], self.parent.resource)
assert issubclass(parent_resource, RestResourceBase)
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 isinstance(self.resource.json_schema_extra, dict)
):
if "plugin" in self.resource.json_schema_extra:
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()
# 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 isinstance(self.resource.json_schema_extra, 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: 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()
# 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
assert isinstance(self.resource, FieldInfo)
current_resource = cast(FieldInfo, self.resource)
current_resource.exclude = True
parent_resource = cast(type[RestResourceBase], self.parent.resource)
assert issubclass(parent_resource, RestResourceBase)
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 isinstance(self.resource.json_schema_extra, dict)
):
if "plugin" in self.resource.json_schema_extra:
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()
# 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

View File

@@ -1,9 +1,6 @@
from __future__ import annotations
from typing import (
cast,
Any,
Optional,
Union,
get_args,
get_origin,
@@ -11,16 +8,16 @@ from typing import (
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
from typing import Any, Optional
TV_RestResourceWalkerFutureResult = TypeVar("TV_RestResourceWalkerFutureResult")
@@ -38,45 +35,44 @@ class RestResourceWalkerFutureResult(ABC, Generic[TV_RestResourceWalkerFutureRes
class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
# pylint: disable=too-many-instance-attributes
cls_RestResourceWalkerFutureResult: Optional[type[RestResourceWalkerFutureResult[TV_RestResourceWalkerFutureResult]]] = None
@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.
"""
...
@classmethod
def get(
self,
cls,
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,
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, argument)
raise RuntimeError(f"Incompatible Field Found: {type(resource).__name__}")
return None
raise RestResourceModelException(f"Incompatible Field Found: {type(resource).__name__}")
def __init__(
self,
resource_name: str,
resource: FieldInfo | Type["RestResourceBase"],
resource: FieldInfo | type[RestResourceBase],
parent: Optional[RestResourceWalker_Sub] = None,
annotation: Optional[type["RestResourceBase"]] = None,
annotation: Optional[type[RestResourceBase]] = None,
_optional: Optional[bool] = None,
argument: Optional[any] = None,
argument: Optional[Any] = None,
):
self.argument: any = argument
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
@@ -85,7 +81,7 @@ 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:
self.annotation, self.optional = self.ProcessAnnotation(resource)
@@ -94,42 +90,17 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
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)
"""
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("-------------------")
"""
@abstractmethod
def get_future(self) -> Optional[RestResourceWalkerFutureResult]:
return self.future_result
def chain_process_future(self) -> Optional[TV_RestResourceWalkerFutureResult]:
if self.future_results_subs is not None and self.future_result is not None:
return_future_results_subs: list[Any] = [] # TODO: use typevar
return_future_results_subs: list[Any] = []
for future_result in self.future_results_subs:
return_future_results_subs.append(future_result.chain_process_future())
return self.future_result.process_future(return_future_results_subs)
@@ -151,11 +122,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
# from .rest_resource import RestResourceBase
_anno: Type[Any]
_anno: type[Any]
# print("!!!!!!!!!!!!!!!!!!!!!!!")
# print(resource)
@@ -166,7 +137,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
@@ -179,14 +150,14 @@ 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
@@ -202,7 +173,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
@@ -212,9 +183,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)),
@@ -230,23 +199,21 @@ 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:
self.subwalker_argument: any = 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, argument: Optional[any] = None, 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
sub_walker_initial: Optional[RestResourceWalker_Sub] = RestResourceWalker_Sub.get(
@@ -256,7 +223,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()
]
@@ -283,6 +250,4 @@ class RestResourceWalker_Root:
resource_list = list(new_resource_list)
current_deep = current_deep + 1
return sub_walker_initial.chain_process_future()
else:
raise RuntimeError("Invalid Rootpoint")
return None
raise RestResourceModelException("Invalid Rootpoint")

View File

@@ -1,7 +1,8 @@
# 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
@@ -57,8 +58,7 @@ TV_SupportedRESTFields = TypeVar(
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")
@@ -91,12 +91,11 @@ _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]
_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"]

23
test/ThreadedUvicorn.py Normal file
View File

@@ -0,0 +1,23 @@
from uvicorn import Config, Server
from threading import Thread
import asyncio
class ThreadedUvicorn:
def __init__(self, config: Config):
self.server = Server(config)
self.thread = Thread(daemon=True, target=self.server.run)
def start(self):
self.thread.start()
asyncio.run(self.wait_for_started())
async def wait_for_started(self):
while not self.server.started:
await asyncio.sleep(0.1)
def stop(self):
if self.thread.is_alive():
self.server.should_exit = True
while self.thread.is_alive():
continue

View File

@@ -5,3 +5,5 @@
#
# 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 .ThreadedUvicorn import ThreadedUvicorn

178
test/test_ACL.py Normal file
View File

@@ -0,0 +1,178 @@
from __future__ import annotations
import unittest
from os import chdir
from pathlib import Path
from typing import Optional
from src.pyrestresource import (
RestField,
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] = RestField(None)
secret: Optional[str] = RestField(
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] = RestField(
"1.2.3",
ACL=[
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
],
)
version: Optional[str] = RestField("3.2.1")
@register_rest_rootpoint
class RootApp(RestResourceBase):
resource_with_secret: TestResource = RestField(default=TestResource())
resource_with_secret_ACL: TestResource = RestField(
default=TestResource(), ACL=[ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY)]
)
resource_ro: TestResource2 = RestField(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)

View File

@@ -1,29 +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 pydantic import Field
from uuid import UUID, uuid4
from time import time, sleep
from time import time
import json
import uvicorn
from typing import Optional, ClassVar
from time import sleep
import socket
import requests
from contextlib import closing
from multiprocessing import Process
from secrets import token_hex
print(__name__)
print(__package__)
from pydantic import BaseModel
import requests
from requests.adapters import HTTPAdapter
import uvicorn
from src.pyrestresource import (
register_rest_rootpoint,
RestField,
ACL_target_user,
UserLogin,
RestResourceBase,
RestResourceBaseLogin,
register_rest_rootpoint,
rsrc_verb,
RestRequestParams_GET,
RestRequestParams_POST,
@@ -32,9 +26,13 @@ from src.pyrestresource import (
T_SupportedRESTFields,
ResourcePlugin_field_default,
ResourcePlugin_RestResourceBase_default,
ACL_target_group_Any,
ACL_record,
ACL_rule,
)
from src.pyrestresource import ACL_target_user, ACL_target_group, ACL_target_group_Any, ACL_record, ACL_rule
from pprint import pprint
from test import ThreadedUvicorn
testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
@@ -42,58 +40,51 @@ chdir(testdir_path.parent.resolve())
# to allow mock-ing, all the tested classes are in a function
def init_classes():
class UserLogin(BaseModel):
username: str
secret: str
token: Optional[str] = None
user_test = UserLogin(username="TestUser", secret="123456")
user_test2 = UserLogin(username="TestUser2", secret="abcdef")
class ResourcePlugin_Login(ResourcePlugin_RestResourceBase_default):
ar_UserLogin: list[UserLogin] = [UserLogin(username="chacha", secret="123456")]
class TestResource(RestResourceBase):
test_field: Optional[str] = RestField("ORIGIN_VALUE")
def handle_resource_get(self, resource: Login, params: RestRequestParams_GET) -> Login:
print("hook GET")
print(resource)
print(params)
return resource
def handle_resource_put(self, resource: Login, params: RestRequestParams_GET) -> Login:
print("hook PUT")
print(resource.username)
print(resource.secret)
for _UserLogin in self.ar_UserLogin:
if _UserLogin.username == resource.username and _UserLogin.secret == resource.secret:
print("user connected")
_UserLogin.token = token_hex(16)
self.set_resp_cookie("test", _UserLogin.token)
print(f"generated token: {_UserLogin.token}")
return resource
print("login NOT found")
# print(resource)
# print(resource.username)
# print(resource.secret)
# print(params)
return resource
class Login(RestResourceBase):
username: Optional[str] = Field(None)
secret: Optional[str] = Field(
None,
exclude=True,
class TestResourceACL(RestResourceBase):
test_field: Optional[str] = RestField(
"ORIGIN_VALUE",
ACL=[
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.ALLOW),
ACL_record(verbs=[rsrc_verb.GET, rsrc_verb.DELETE, rsrc_verb.POST], target=ACL_target_group_Any(), rule=ACL_rule.DENY),
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] = RestField(
"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] = RestField(
"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 = RestField(
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
@@ -101,90 +92,534 @@ 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)
class Test_RestAPI_LOGIN(unittest.TestCase):
def setUp(self) -> None:
chdir(testdir_path.parent.resolve())
init_classes()
self.testapp = RootApp()
def test_login(self):
"""
result = self.testapp.process_request("/login", rsrc_verb.GET)
print("*****************")
print(result.get_result())
result = self.testapp.process_request("/login/username", rsrc_verb.GET)
print("*****************")
print(result.get_result())
# result = self.testapp.process_request("/login/secret", rsrc_verb.GET)
# print("*****************")
# print(result.get_result())
"""
result = self.testapp.process_request("/login", rsrc_verb.PUT, '{"username":"chacha","secret":"123456"}')
print("*****************")
print(result.get_result())
"""
result = self.testapp.process_request("/login", rsrc_verb.GET)
print("*****************")
print(result.get_result())
result = self.testapp.process_request("/login/username", rsrc_verb.GET)
print("*****************")
print(result.get_result())
# result = self.testapp.process_request("/login/secret", rsrc_verb.GET)
# print("*****************")
# print(result.get_result())
"""
class Test_RestAPI_LOGIN_Web(unittest.TestCase):
def setUp(self) -> None:
chdir(testdir_path.parent.resolve())
def test_login(self):
def test_login_two_users(self):
ip, port = find_free_port()
print(f"ip1={ip}")
print(f"port1={port}")
proc = Process(
target=launch_server,
args=(
ip,
port,
),
)
proc.start()
sleep(1)
s = requests.Session()
try:
# Login in
init_classes()
server = ThreadedUvicorn(uvicorn.Config(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True))
server.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": "chacha", "secret": "123456"},
json={"username": "TestUser", "secret": "123456"},
)
print(response)
print(response.headers)
self.assertEqual(response.status_code, 201)
response = s.get(f"http://{ip}:{port}/login")
# 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)
response = s.get(f"http://{ip}:{port}/")
# 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()
server.stop()
def test_login(self):
ip, port = find_free_port()
init_classes()
server = ThreadedUvicorn(uvicorn.Config(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True))
server.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__"})
# 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:
s.close()
server.stop()
def test_change_host(self):
ip, port = find_free_port()
init_classes()
server = ThreadedUvicorn(uvicorn.Config(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True))
server.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:
s1.close()
s2.close()
server.stop()
def test_login_wrong_pwd(self):
ip, port = find_free_port()
init_classes()
server = ThreadedUvicorn(uvicorn.Config(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True))
server.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:
s.close()
server.stop()
def test_access_resourceACL(self):
ip, port = find_free_port()
init_classes()
server = ThreadedUvicorn(uvicorn.Config(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True))
server.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:
s.close()
server.stop()
def test_access_fieldACL(self):
ip, port = find_free_port()
init_classes()
server = ThreadedUvicorn(uvicorn.Config(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True))
server.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:
s.close()
server.stop()

View File

@@ -4,16 +4,13 @@ from unittest.mock import patch
from os import chdir
from pathlib import Path
from typing import Optional
from pydantic import Field
from uuid import UUID, uuid4
from time import time
import json
print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
RestResourceHandlerException_Forbiden,
register_rest_rootpoint,
RestResourceBase,
rsrc_verb,
@@ -21,6 +18,9 @@ from src.pyrestresource import (
RestRequestParams_POST,
RestRequestParams_Dict_GET,
T_SupportedRESTFields,
ACL_target_group_Any,
ACL_record,
ACL_rule,
)
from pprint import pprint
@@ -35,19 +35,19 @@ def init_classes():
api_version: str
class Patch(RestResourceBase):
uuid: UUID = Field(default_factory=uuid4, primary_key=True)
uuid: UUID = RestField(default_factory=uuid4, primary_key=True)
shortname: str
name: Optional[str] = None
description: Optional[str] = None
class Profile(RestResourceBase):
uuid: UUID = Field(default_factory=uuid4, primary_key=True)
uuid: UUID = RestField(default_factory=uuid4, primary_key=True)
shortname: str
name: Optional[str] = None
description: Optional[str] = None
class Game(RestResourceBase):
uuid: UUID = Field(default_factory=uuid4, primary_key=True)
uuid: UUID = RestField(default_factory=uuid4, primary_key=True)
shortname: str
name: Optional[str] = None
description: Optional[str] = None
@@ -58,9 +58,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 = RestField(
default_factory=uuid4,
primary_key=True,
)
name: str
secret: str = Field(..., exclude=True)
secret: str = RestField(
...,
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,10 +78,8 @@ 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)
uuid: UUID = RestField(default_factory=uuid4, primary_key=True)
shortname: str
name: Optional[str] = None
description: Optional[str] = None
@@ -117,100 +125,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 +226,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 +255,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 +286,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 +296,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 +315,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 +333,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 +348,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 +364,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 +383,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 +405,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 +419,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 +427,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 +454,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 +464,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 +480,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 +503,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 +515,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 +533,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 +542,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 +551,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,

View File

@@ -1,18 +1,11 @@
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 pydantic import Field
from uuid import UUID, uuid4
from time import time
import json
print(__name__)
print(__package__)
from typing import Annotated
from src.pyrestresource import (
RestField,
register_rest_rootpoint,
RestResourceBase,
rsrc_verb,
@@ -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())
@@ -47,27 +40,27 @@ def init_classes():
class Info_get(RestResourceBase):
# test plugin injection within annotation
# + test plugin on a simple field
version: Annotated[str, Field(plugin=ResourcePlugin_version_get)]
version: Annotated[str, RestField(plugin=ResourcePlugin_version_get)]
api_version: str
class Info_put(RestResourceBase):
# test plugin injection within annotation
# + test plugin on a simple field
version: Annotated[str, Field(plugin=ResourcePlugin_version_put)]
version: Annotated[str, RestField(plugin=ResourcePlugin_version_put)]
api_version: str
@register_rest_rootpoint
class RootApp(RestResourceBase):
# test plugin injection within Field value
# + test plugin on a RestResourceBase field
info: Info_get = Field(
info: Info_get = RestField(
default=Info_get(version="0.0.1", api_version="0.0.2"),
plugin=ResourcePlugin_Info,
)
info_put: Info_put = Field(
info_put: Info_put = RestField(
default=Info_put(version="0.0.1", api_version="0.0.2"),
)
info2: Info_get = Field(default=Info_get(version="0.0.2", api_version="0.0.3"))
info2: Info_get = RestField(default=Info_get(version="0.0.2", api_version="0.0.3"))
# 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
@@ -77,48 +70,16 @@ 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)]
tetvaluestr: Annotated[str, RestField(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)]
@register_rest_rootpoint
class RootApp2(RestResourceBase):
test: TestResource = Field(default=TestResource(tetvaluestr="testvalue"))
test: TestResource = RestField(default=TestResource(tetvaluestr="testvalue"))
RootApp2()
@@ -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()

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,18 +1,13 @@
from __future__ import annotations
import unittest
from typing import Optional, cast
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__)
from contextlib import redirect_stdout
from src.pyrestresource import (
RestField,
RestResourceBase,
)
@@ -80,7 +75,7 @@ def init_classes():
last_name: str
class RootApp(RestResourceBase):
info: Info = Field(default=Info(version="0.0.1", api_version="0.0.2"))
info: Info = RestField(default=Info(version="0.0.1", api_version="0.0.2"))
info2: Info = Info(version="0.0.2", api_version="0.0.3")
peoples: dict[str, People] = {
"john": People(last_name="Doe"),

View File

@@ -1,18 +1,11 @@
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__)
from src.pyrestresource import (
RestField,
RestResourceBase,
)
@@ -81,7 +74,7 @@ def init_classes():
last_name: str
class RootApp(RestResourceBase):
info: Info = Field(default=Info(version="0.0.1", api_version="0.0.2"))
info: Info = RestField(default=Info(version="0.0.1", api_version="0.0.2"))
info2: Info = Info(version="0.0.2", api_version="0.0.3")
peoples: dict[str, People] = {
"john": People(last_name="Doe"),

View File

@@ -1,24 +1,19 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from os import chdir
from pathlib import Path
from typing import Optional
from pydantic import Field
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
print(__name__)
print(__package__)
import requests
from requests.adapters import HTTPAdapter
import uvicorn
from src.pyrestresource import (
RestField,
register_rest_rootpoint,
RestResourceBase,
rsrc_verb,
@@ -27,7 +22,8 @@ from src.pyrestresource import (
RestRequestParams_Dict_GET,
T_SupportedRESTFields,
)
from pprint import pprint
from test import ThreadedUvicorn
testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
@@ -40,19 +36,19 @@ def init_classes():
api_version: str
class Patch(RestResourceBase):
uuid: UUID = Field(default_factory=uuid4, primary_key=True)
uuid: UUID = RestField(default_factory=uuid4, primary_key=True)
shortname: str
name: Optional[str] = None
description: Optional[str] = None
class Profile(RestResourceBase):
uuid: UUID = Field(default_factory=uuid4, primary_key=True)
uuid: UUID = RestField(default_factory=uuid4, primary_key=True)
shortname: str
name: Optional[str] = None
description: Optional[str] = None
class Game(RestResourceBase):
uuid: UUID = Field(default_factory=uuid4, primary_key=True)
uuid: UUID = RestField(default_factory=uuid4, primary_key=True)
shortname: str
name: Optional[str] = None
description: Optional[str] = None
@@ -63,9 +59,9 @@ 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 = RestField(default_factory=uuid4, primary_key=True)
name: str
secret: str = Field(..., exclude=True)
secret: str = RestField(..., exclude=True)
User1 = User(
uuid="8da57a3c-661f-11ee-8c99-0242ac120002",
@@ -74,7 +70,7 @@ def init_classes():
)
class Patch2(RestResourceBase):
uuid: UUID = Field(default_factory=uuid4, primary_key=True)
uuid: UUID = RestField(default_factory=uuid4, primary_key=True)
shortname: str
name: Optional[str] = None
description: Optional[str] = None
@@ -117,35 +113,23 @@ 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)
class Test_RestAPI_WebServer(unittest.TestCase):
def setUp(self) -> None:
chdir(testdir_path.parent.resolve())
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=(
ip,
port,
),
)
proc.start()
init_classes()
server = ThreadedUvicorn(uvicorn.Config(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True))
server.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 +141,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",
@@ -284,27 +259,23 @@ class Test_RestAPI_WebServer(unittest.TestCase):
data = response.json()
self.assertTrue(len(data) == 0)
finally:
proc.terminate()
s.close()
server.stop()
# @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=(
ip,
port,
),
)
proc.start()
init_classes()
server = ThreadedUvicorn(uvicorn.Config(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True))
server.start()
sleep(1)
s = requests.Session()
s.mount("http://", HTTPAdapter(max_retries=0))
try:
start = time()
for _ in range(n_loop):
@@ -378,5 +349,5 @@ class Test_RestAPI_WebServer(unittest.TestCase):
print(f"PUT/GET 2nd level (value) dict: {int(n_loop/(end-start))} Req/s")
finally:
proc.terminate()
s.close()
server.stop()