12 Commits

Author SHA1 Message Date
27c576db21 Merge pull request 'dev' (#1) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pyrestresource/pulls/1
new-tag:0.1.0
2023-11-06 16:07:12 +01: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
28 changed files with 1068 additions and 412 deletions

14
Jenkinsfile vendored
View File

@@ -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",

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,34 @@ include-package-data = true
where = ["src"]
[tool.setuptools.package-data]
"pyrestresource.data" = ["*.*"]
"pyrestresource" = ["py.typed"]
# [[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'
]
[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,23 +18,9 @@ from typing import TYPE_CHECKING
from .__metadata__ import __version__, __Summuary__, __Name__
from .rest_resource import 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,
RestResourceException,
)
from .rest_request_opt import (
RestRequestParams_POST,
RestRequestParams_DELETE,
@@ -46,13 +32,34 @@ from .rest_request_opt import (
RestRequestParams_Dict_DELETE,
RestRequestParams_Dict_GET,
)
from .rest_resource_plugin import (
ResourcePlugin_field_default,
ResourcePlugin_RestResourceBase_default,
ResourcePlugin_dict_default,
RestResourcePluginException,
RestResourcePluginException_InvalidPluginSignature,
)
from .rest_ACL import ACL_target_user, ACL_target_group, ACL_target_group_Any, ACL_record, ACL_rule
from .rest_login import RestResourceBaseLogin, UserLogin
from .rest_resource import RestResourceBase
from .rest_login import (
RestResourceBaseLogin,
UserLogin,
)
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

@@ -4,6 +4,7 @@ from __future__ import annotations
from uuid import UUID
import json
import traceback
from .rest_types import T_Gen_DictKeys
@@ -18,8 +19,8 @@ class _JSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, o)
def parse_dict_cookies(cookies: str) -> dict[str, str]:
result = {}
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:
@@ -30,3 +31,10 @@ def parse_dict_cookies(cookies: str) -> dict[str, str]:
name, value = item.split("=", 1)
result[name] = value
return result
def forward_exception(e: Exception, forward: bool) -> None:
if forward:
raise e from None
else:
traceback.print_exc()

View File

@@ -22,7 +22,7 @@ class ACL_target_user(ACL_target):
return cls(name=user_login.username)
class ACL_target_user_Annonymous(ACL_target):
class ACL_target_user_Annonymous(ACL_target_user):
name: str = "__ANNONYMOUS__"

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

@@ -15,16 +15,24 @@ from __future__ import annotations
from typing import Optional, ClassVar, TYPE_CHECKING
from secrets import token_hex, compare_digest
from datetime import datetime
from pydantic import BaseModel, Field
from datetime import datetime, timedelta
from pydantic import BaseModel
from .rest_types import rsrc_verb
from .rest_resource import RestResourceBase
from .rest_model import RestField
from .rest_ACL import ACL_record, ACL_target_group_Any, ACL_rule, ACL_target_user
from .rest_resource_plugin import ResourcePlugin_RestResourceBase_default
from .rest_exceptions import (
RestResourceLoginException_InvalidCredentials,
RestResourceLoginException_ClientChange,
RestResourceLoginException_SessionTimeout,
RestResourceLoginException_InvalidSession,
)
if TYPE_CHECKING is True:
from .rest_request import RestRequest, RestRequestParams_GET
from .rest_request import RestRequest
from .rest_request_opt import RestRequestParams_RestResourceBase_PUT, RestRequestParams_RestResourceBase_GET
class UserLogin(BaseModel):
@@ -35,24 +43,26 @@ class UserLogin(BaseModel):
class UserSession(BaseModel):
last_update: datetime
user_login: UserLogin
host: Optional[str]
client: tuple[str, int] | tuple[()] | None
class ResourcePlugin_Login(ResourcePlugin_RestResourceBase_default):
ar_UserLogin: list[UserLogin] = []
def handle_resource_get(self, resource: Login, params: RestRequestParams_GET) -> Login:
return Login(username=self.get_user_login())
def handle_resource_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_GET) -> Login:
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
class Login(RestResourceBase):
username: Optional[str] = Field(None)
secret: Optional[str] = Field(
username: Optional[str] = RestField(None)
secret: Optional[str] = RestField(
None,
exclude=True,
ACL=[
@@ -65,30 +75,38 @@ class Login(RestResourceBase):
class RestResourceBaseLogin(RestResourceBase):
_ar_user_login: ClassVar[list[UserLogin]] = []
_ar_user_session: dict[str, UserSession] = {}
_max_session_time_minutes: ClassVar[int] = 20
login: Login = Field(default=Login(), plugin=ResourcePlugin_Login)
_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 != None:
if auth_cookie in self._ar_user_session:
print("USER SESSION FOUND !")
print(self._ar_user_session[auth_cookie].user_login.username)
print(auth_cookie)
# print(f"SESSION FOUND for {request.get_client()}")
time_diff_min = (datetime.now() - self._ar_user_session[auth_cookie].last_update).total_seconds() / 60
if time_diff_min > self._max_session_time_minutes:
if self._ar_user_session[auth_cookie].client != request.get_client():
del self._ar_user_session[auth_cookie]
raise RuntimeError("session timeout ! (session reseted)")
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
print("Invalid session")
raise RestResourceLoginException_InvalidSession()
return
print("non-connected user")
# print(f"non-connected user {request.get_client()}")
def user_login(self, user_name: str, user_secret: str, request: RestRequest) -> str:
already_failed: bool = False
@@ -106,11 +124,10 @@ class RestResourceBaseLogin(RestResourceBase):
pass
pass
if already_failed:
raise RuntimeError("Wrong auth") # TODO: specific exception
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, host=request.get_host())
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

@@ -0,0 +1,103 @@
from __future__ import annotations
from typing import (
Any,
Literal,
Callable,
Optional,
TYPE_CHECKING,
)
from pydantic.fields import Field, _Unset, PydanticUndefined
from .rest_exceptions import RestResourceModelException
if TYPE_CHECKING is True:
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(
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,
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: Optional[list[ACL_record]] = _Unset,
plugin: Optional[type[ResourcePlugin]] = _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

@@ -13,6 +13,7 @@ from urllib.parse import urlparse, parse_qs
from pydantic import BaseModel, Field
from typeguard import check_type
from .rest_login import RestResourceBaseLogin
from .rest_types import rsrc_verb, T_AllSupportedFields
from .rest_request_opt import (
RestRequestParams_POST,
@@ -27,10 +28,17 @@ from .rest_request_opt import (
)
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,
)
if TYPE_CHECKING is True:
from typing import Optional
from .rest_types import T_SupportedRESTFields
from .rest_resource import RestResourceBase
class RequestFactory(
@@ -49,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:
@@ -60,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
@@ -85,7 +95,7 @@ class RequestFactory(
elif request.verb is rsrc_verb.DELETE:
request.update_ReqParams(self.cls_RestRequestParams_DELETE)
else:
raise RuntimeError("Invalid Verb")
raise RestResourceHandlerException_MethodNotAllowed("Invalid Verb")
return
@@ -96,12 +106,11 @@ class RestRequest(Generic[_T_RestRequestParams]):
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
@@ -118,27 +127,29 @@ class RestRequest(Generic[_T_RestRequestParams]):
self.url: str
self.verb: rsrc_verb
self.data: dict
self.raw_headers: list[Any]
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.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 RuntimeError(f"Wrong data type received: {data}")
raise RestResourceHandlerException_BadRequest(f"Wrong data type received: {data}")
self.data = data
@@ -157,23 +168,59 @@ 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:
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"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
return self.headers["cookie"][key]
if isinstance(self.headers["cookie"], dict):
return self.headers["cookie"][key]
else:
return None
def get_host(self) -> str:
print(self.headers["host"])
def set_resp_cookie_value(self, key: str, value: str) -> None:
if not isinstance(self.root_resource, RestResourceBaseLogin):
raise RestResourceConfigException("root_resource must be RestResourceBaseLogin to use user_login")
self.outgoing_cookie[
key
] = f"{value}; expires={self.root_resource.get_new_cookie_expiration_date().strftime('%a, %d %b %Y %H:%M:%S GMT')}; path=/; HttpOnly"
def reset_resp_cookie(self, key: str) -> None:
self.outgoing_cookie[key] = "null; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
def get_host(self) -> str | dict[str, str | None] | None:
return self.headers["host"]
def set_result(self, result: str):
self.result = result

View File

@@ -8,10 +8,12 @@ from typing import (
from abc import ABC
import json
import pprint
from pydantic import BaseModel
from .helpers import _JSONEncoder
from .rest_types import rsrc_verb, _T_SupportedRESTFields
from .rest_types import rsrc_verb
from .helpers import _JSONEncoder, forward_exception
from .rest_ACL import (
ACL_record,
@@ -21,9 +23,22 @@ from .rest_ACL import (
ACL_rule,
)
from .rest_request import RestRequest
from .rest_exceptions import (
RestResourceLoginException_InvalidSession,
RestResourceLoginException_SessionTimeout,
RestResourceLoginException_ClientChange,
RestResourceLoginException_InvalidCredentials,
RestResourceHandlerException_ResourceNotFound,
RestResourceHandlerException_MethodNotAllowed,
RestResourceHandlerException_BadRequest,
RestResourceHandlerException_Forbiden,
RestResourceException,
)
if TYPE_CHECKING is True:
from .rest_request import RestRequest
from .rest_types import T_SupportedRESTFields
from .rest_resource_plugin import ResourcePlugin
from .rest_types import (
T_T_DictKey,
T_T_DictValues,
@@ -31,7 +46,6 @@ if TYPE_CHECKING is True:
class RestResourceBase(ABC, BaseModel, validate_assignment=True):
# _resp_cookies: ClassVar[dict[str, str]] = {}
_dict_key_type_: ClassVar[dict[str, T_T_DictKey]] = {}
_dict_value_type_: ClassVar[dict[str, T_T_DictValues]] = {}
_model_dump_excluded_: ClassVar[dict[str, bool]] = {}
@@ -39,13 +53,13 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
_plugins_: ClassVar[
dict[
str,
list[ACL_record],
type[ResourcePlugin],
]
] = {}
_ACL_record_: ClassVar[
dict[
str,
ACL_record,
list[ACL_record],
]
] = {}
@@ -64,32 +78,33 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
if acl.rule is ACL_rule.ALLOW:
# 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 isinstance(acl.target, ACL_target_group_Any) or any(_ for _ in groups if _.name == acl.target.name):
if acl.rule is ACL_rule.ALLOW:
# print("ALLOWED (group)")
return
raise 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}")
raise RestResourceException(f"Wrong ACL target type: {field}")
# print("ALLOWED (Default)")
def check_acl_field(self, request: RestRequest, req_index: int = 0) -> None:
"""Check ACL on requested field access"""
self._check_acl(request.user, request.groups, request.get_verb(), request.get_resource_origin(req_index), False)
def check_acl_self(self, request: RestRequest, new_data: Optional[dict[str, _T_SupportedRESTFields]]) -> None:
def check_acl_self(self, request: RestRequest, new_data: Optional[dict[str, T_SupportedRESTFields]]) -> None:
"""Check ACL on requested field operation (involving checking sub-fields)"""
if request.get_verb() is rsrc_verb.GET:
for key in self.model_fields.keys():
self._check_acl(request.user, request.groups, rsrc_verb.GET, key)
elif request.get_verb() is rsrc_verb.PUT:
for key in new_data.keys():
if key in self.model_fields:
self._check_acl(request.user, request.groups, rsrc_verb.PUT, key)
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():
@@ -109,7 +124,7 @@ 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"]
@@ -119,27 +134,25 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
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["headers"]
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"],
],
@@ -148,13 +161,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(
{
@@ -172,7 +184,9 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
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,
@@ -188,22 +202,50 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
# preparing request & session
request: RestRequest = ressource_handler.get_request()
assert request is not None
if headers is not None:
request.set_headers(headers)
if client is not None:
request.set_client(client)
try:
self._process_request_session(request)
# processing the verb
result = ressource_handler.process_verb()
result = ressource_handler.process_verb()
# print("OOO")
# print(type(self)._resp_cookies)
# print("OOO2")
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")
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

@@ -4,6 +4,7 @@ from typing import Optional, cast, TypeVar, Generic, Self, TYPE_CHECKING
import abc
from .rest_types import (
NoneType,
rsrc_verb,
T_SupportedRESTFields,
T_DictKey,
@@ -15,6 +16,7 @@ from .rest_resource import RestResourceBase
from .rest_request import RequestFactory
from .rest_resource_plugin import (
ResourcePlugin_field,
ResourcePlugin_dict,
ResourcePlugin_RestResourceBase,
)
from .rest_request_opt import (
@@ -33,6 +35,14 @@ from .rest_request_opt import (
_T_RestRequestParams_PUT,
)
from .rest_exceptions import (
RestResourceHandlerException,
RestResourceHandlerException_ResourceNotFound,
RestResourceHandlerException_MethodNotAllowed,
RestResourceHandlerException_BadRequest,
RestResourceHandlerException_Forbiden,
)
if TYPE_CHECKING is True:
from .rest_types import T_T_DictKey, T_T_DictValues
from .rest_request import RestRequest
@@ -83,7 +93,6 @@ class ResourceHandler(
self.next_handler: Optional[ResourceHandler] = None
self.saved_url: list[str] = []
self.resource: _T_Resource = resource
self.root_resource: _T_Resource = resource if prev_handler is None else prev_handler.root_resource
self.req: RestRequest
if prev_handler is not None:
self.prev_handler = prev_handler
@@ -91,13 +100,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, 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()}")
@@ -116,7 +125,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:
@@ -187,7 +196,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
@@ -210,7 +219,7 @@ class ResourceHandler(
self._process_delete()
return None
raise RuntimeError("Invalid Verb")
raise RestResourceHandlerException_BadRequest("Invalid Verb")
def _process_get(
self,
@@ -231,16 +240,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
@@ -289,8 +298,7 @@ class ResourceHandler_dict(
# print(f"{type(self).__name__}->_handle_process_delete()")
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
if self.prev_handler is None:
raise RuntimeError("Wrong command")
assert self.prev_handler is not None
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(1)]
@@ -308,15 +316,19 @@ class ResourceHandler_dict(
# print(f"{type(self).__name__}->_handle_process_post()")
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
if self.prev_handler is None:
raise RuntimeError("Wrong command")
assert self.prev_handler is not None
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(1)]
dict_value_type: T_T_DictValues = cast(RestResourceBase, self.prev_handler.resource)._dict_value_type_[
self.req.get_resource_origin(1)
]
_obj = dict_value_type(**self.req.get_data())
_obj: T_DictValues
if not issubclass(dict_value_type, NoneType): # type: ignore # => mypy bug with type[None]
_obj = dict_value_type(**self.req.get_data()) # type: ignore # => mypy bug with type[None]
else:
_obj = None
_dict: dict[T_DictKey, T_DictValues] = cast(dict[T_DictKey, T_DictValues], self.resource)
@@ -341,7 +353,9 @@ class ResourceHandler_dict(
_dict[_obj_primary_key] = _obj
return _obj_primary_key
RuntimeError("Either the object needs defined primary key or the request must contain an API_key param to process this command")
raise RestResourceHandlerException_BadRequest(
"Either the object needs defined primary key or the request must contain an API_key param to process this command"
)
return None # for mypy....
@@ -381,8 +395,7 @@ class ResourceHandler_dict_elem(
# print(f"{type(self).__name__}->_process_get()")
# print(f"{type(self).__name__}->resource = {type(self.resource).__name__}")
if self.prev_handler is None:
raise RuntimeError("Wrong command")
assert self.prev_handler is not None
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(1)]
@@ -401,8 +414,7 @@ class ResourceHandler_dict_elem(
# instead of expected get_resource_origin(1) because we need to go backward
# because self.req is another context that is not saved to improve performances
if self.prev_handler is None:
raise RuntimeError("Wrong command")
assert self.prev_handler is not None
dict_key_type: T_T_DictKey = cast(RestResourceBase, self.prev_handler.resource)._dict_key_type_[self.req.get_resource_origin(2)]
@@ -460,13 +472,13 @@ class ResourceHandler_RestResourceBase(
# 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_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()}")
raise RestResourceHandlerException_ResourceNotFound(f"Not allowed READ access detected: {self.req.get_url_stack()}")
def _handle_process_get(self, params) -> RestResourceBase:
# print(f"{type(self).__name__}->_process_get()")
@@ -474,22 +486,23 @@ class ResourceHandler_RestResourceBase(
# 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_self(self.req, None)
for key, attr in self.resource.model_fields.items():
if key in self.resource._plugins_:
if issubclass(self.resource._plugins_[key], ResourcePlugin_field):
plugin_field: ResourcePlugin_field = cast(
ResourcePlugin_field, self.resource._plugins_[key](self.req, self.root_resource)
)
plugin_field = cast(ResourcePlugin_field, self.resource._plugins_[key](self.req, self.req.get_root_resource()))
value = getattr(self.resource, key)
setattr(self.resource, key, plugin_field.handle_field_get(value, params))
elif issubclass(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
plugin_field: ResourcePlugin_field = cast(
ResourcePlugin_RestResourceBase, self.resource._plugins_[key](self.req, self.root_resource)
plugin_resource = cast(
ResourcePlugin_RestResourceBase, self.resource._plugins_[key](self.req, self.req.get_root_resource())
)
value = getattr(self.resource, key)
setattr(self.resource, key, plugin_field.handle_resource_get(value, params))
setattr(self.resource, key, plugin_resource.handle_resource_get(value, params))
# result = RestResourceWalker_Root__handler(self.resource).process()
# print(result)
@@ -507,18 +520,18 @@ class ResourceHandler_RestResourceBase(
key = self.req.get_resource_origin(0)
if key in self.resource._plugins_:
if issubclass(self.resource._plugins_[key], ResourcePlugin_field):
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
ResourcePlugin_RestResourceBase,
self.resource._plugins_[key](self.req, self.root_resource),
plugin_field = cast(
ResourcePlugin_field,
self.resource._plugins_[key](self.req, self.req.get_root_resource()),
)
value = plugin_rsrc.handle_field_get(value, params)
value = plugin_field.handle_field_get(value, params)
elif issubclass(self.resource._plugins_[key], ResourcePlugin_RestResourceBase):
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
plugin_resource = cast(
ResourcePlugin_RestResourceBase,
self.resource._plugins_[key](self.req, self.root_resource),
self.resource._plugins_[key](self.req, self.req.get_root_resource()),
)
value = plugin_rsrc.handle_resource_get(value, params)
value = plugin_resource.handle_resource_get(value, params)
return value
@@ -539,32 +552,33 @@ class ResourceHandler_RestResourceBase(
if key in _new_resrc._plugins_:
if issubclass(_new_resrc._plugins_[key], ResourcePlugin_field):
plugin_field: ResourcePlugin_field = cast(
ResourcePlugin_field, _new_resrc._plugins_[key](self.req, self.root_resource)
ResourcePlugin_field, _new_resrc._plugins_[key](self.req, self.req.get_root_resource())
)
value = getattr(_new_resrc, key)
setattr(_new_resrc, key, plugin_field.handle_field_put(value, params))
# applying plugins (from parent element)
if self.prev_handler is not None:
# element is within a dict
if (
isinstance(self.prev_handler.resource, dict) # element is within a dict
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, self.root_resource),
plugin_dict: ResourcePlugin_dict = cast(
ResourcePlugin_dict,
self.prev_handler.prev_handler.resource._plugins_[key](self.req, self.req.get_root_resource()),
)
_new_resrc = plugin_rsrc.handle_dict_elem_put(_new_resrc, params)
_new_resrc = plugin_dict.handle_dict_elem_put(_new_resrc, params)
# element is within a RestResourceBase
elif isinstance(self.prev_handler.resource, RestResourceBase):
key = self.req.get_resource_origin(1)
if key in self.prev_handler.resource._plugins_:
plugin_rsrc: ResourcePlugin_RestResourceBase = cast(
ResourcePlugin_RestResourceBase,
self.prev_handler.resource._plugins_[key](self.req, self.root_resource),
self.prev_handler.resource._plugins_[key](self.req, self.req.get_root_resource()),
)
_new_resrc = plugin_rsrc.handle_resource_put(_new_resrc, params)
@@ -584,7 +598,7 @@ class ResourceHandler_RestResourceBase(
):
self.prev_handler._process_delete()
else:
raise RuntimeError("cannot delete an element outside a dict")
raise RestResourceHandlerException_BadRequest("cannot delete an element outside a dict")
@ResourceHandler.register_resource_handler
@@ -615,7 +629,7 @@ class ResourceHandler_simple(
if self.req.get_resource_origin(1) in self.prev_handler.resource._plugins_:
plugin_simple: ResourcePlugin_field = cast(
ResourcePlugin_field,
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)](self.req, self.root_resource),
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)](self.req, self.req.get_root_resource()),
)
return plugin_simple.handle_field_get(self.resource, params)
@@ -636,7 +650,7 @@ class ResourceHandler_simple(
# print("PLUGIN FOUND")
plugin_simple: ResourcePlugin_field = cast(
ResourcePlugin_field,
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)](self.req, self.root_resource),
self.prev_handler.resource._plugins_[self.req.get_resource_origin(1)](self.req, self.req.get_root_resource()),
)
# print(value)
value = plugin_simple.handle_field_put(value, params)

View File

@@ -23,40 +23,43 @@ from .rest_resource_walker import (
)
if TYPE_CHECKING is True:
from typing import Optional
from typing import Optional, Any
class RestResourceWalkerFutureResult_RestResourceBase_handler(RestResourceWalkerFutureResult[dict]):
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
print(f"RestResourceWalkerFutureResult_RestResourceBase_handler {result}")
res = {}
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
# print(f"RestResourceWalkerFutureResult_RestResourceBase_handler {result}")
res: dict[str, Any] = {}
res[self.source.resource_name] = {}
if result:
for subres in result:
key = next(iter(subres))
print(key)
res[self.source.resource_name] = res[self.source.resource_name] | subres
return res
class RestResourceWalkerFutureResult_Dict_handler(RestResourceWalkerFutureResult[dict]):
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
print(f"RestResourceWalkerFutureResult_Dict_handler {result}")
res = {}
for subres in result:
res = res | subres
# print(f"RestResourceWalkerFutureResult_Dict_handler {result}")
res: dict[str, Any] = {}
if result:
for subres in result:
res = res | subres
return res
class RestResourceWalkerFutureResult_RestFields_handler(RestResourceWalkerFutureResult[dict]):
def process_future(self, result: Optional[list[dict]]) -> Optional[dict]:
print(f"RestResourceWalkerFutureResult_RestFields_handler {result}")
print(self.source.resource)
res = {}
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
# print(f"RestResourceWalkerFutureResult_RestFields_handler {result}")
# print(self.source.resource)
res: dict[str, Any] = {}
res[self.source.resource_name] = {}
if result:
for subres in result:
key = next(iter(subres))
print(key)
res[self.source.resource_name] = res[self.source.resource_name] | subres
return res

View File

@@ -2,17 +2,19 @@ from __future__ import annotations
from typing import Optional, Generic, TYPE_CHECKING
from abc import abstractmethod, ABC
from datetime import datetime
from .rest_types import (
_T_DictValues,
_T_DictKey,
TV_SupportedRESTFields,
TV_RestResourceBase,
RestResourceException,
)
from .rest_request import RestRequest
from .rest_exceptions import RestResourceConfigException
if TYPE_CHECKING is True:
from .rest_request import RestRequest
from .rest_resource import RestResourceBase
from .rest_request_opt import (
RestRequestParams_GET,
@@ -27,35 +29,36 @@ if TYPE_CHECKING is True:
)
class RestResourcePluginException(RestResourceException):
pass
class RestResourcePluginException_InvalidPluginSignature(RestResourcePluginException):
pass
class ResourcePlugin(ABC):
def __init__(self, request: RestRequest, root_resource: RestResourceBase) -> None:
self.__request: RestRequest = request
self.__root_resource: RestRequest = root_resource
self.__root_resource: RestResourceBase = root_resource
def user_login(self, user_name: str, user_secret: str) -> str:
from .rest_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 getr_req_cookie_value(self, key: str) -> Optional[str]:
return self.__request.incoming_cookie[key]
def set_resp_cookie_value(self, key: str, value: str) -> None:
self.__request.set_resp_cookie_value(key, value)
def set_resp_cookie_value(self, key: str, value: str):
# print("AAA")
# print(name)
# print(value)
# print(self.cookies)
# print(type(self.cookies))
self.__request.outgoing_cookie[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_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)
class ResourcePlugin_field(ResourcePlugin, Generic[TV_SupportedRESTFields]):

View File

@@ -1,10 +1,12 @@
from __future__ import annotations
from typing import (
cast,
get_args,
get_origin,
TYPE_CHECKING,
)
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from .rest_resource import RestResourceBase
@@ -12,7 +14,6 @@ from .rest_resource_plugin import (
ResourcePlugin_field,
ResourcePlugin_RestResourceBase,
ResourcePlugin_dict,
RestResourcePluginException_InvalidPluginSignature,
)
from .rest_resource_walker import (
RestResourceWalker_Root,
@@ -26,6 +27,7 @@ from .rest_ACL import (
ACL_target_group_Any,
ACL_rule,
)
from .rest_exceptions import RestResourcePluginException_InvalidPluginSignature, RestResourceModelException, RestResourceModelException_ACL
if TYPE_CHECKING is True:
pass
@@ -37,9 +39,9 @@ class RestResourceWalker_Sub_T_Dict__tree_init(RestResourceWalker_Sub_T_Dict):
# checking compatibility
if not get_origin(datatype[1]) is None:
raise RuntimeError("complex dict types are not supported (should create a RestResourceBase container)")
raise RestResourceModelException("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)")
raise RestResourceModelException(f"Unsupported Dict Field value type in class (key)")
# preprocessing types / structure
if self.parent is not None and isinstance(self.parent, RestResourceWalker_Sub_RestResourceBase):
@@ -47,8 +49,13 @@ class RestResourceWalker_Sub_T_Dict__tree_init(RestResourceWalker_Sub_T_Dict):
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)
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] = []
@@ -58,7 +65,7 @@ class RestResourceWalker_Sub_T_Dict__tree_init(RestResourceWalker_Sub_T_Dict):
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"]
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
@@ -69,10 +76,10 @@ class RestResourceWalker_Sub_T_Dict__tree_init(RestResourceWalker_Sub_T_Dict):
# 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()")
raise RestResourceModelException_ACL("ACL must be a list()")
else:
raise RuntimeError("dict must be contained in a RestResourceBase")
raise RestResourceModelException("dict must be contained in a RestResourceBase")
class RestResourceWalker_Sub_RestFields__tree_init(RestResourceWalker_Sub_RestFields):
@@ -96,14 +103,16 @@ class RestResourceWalker_Sub_RestFields__tree_init(RestResourceWalker_Sub_RestFi
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}")
raise RestResourceModelException(
f"Only one primary key is allowed {self.parent.resource_name}.{self.resource_name}"
)
self.parent.annotation._primary_key_ = self.resource_name
self.parent.annotation._ACL_record_[self.resource_name] = [
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_group_Any(), rule=ACL_rule.DENY)
]
if "plugin" in self.resource.json_schema_extra:
plugin_field: ResourcePlugin_field = self.resource.json_schema_extra["plugin"]
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
@@ -114,10 +123,10 @@ class RestResourceWalker_Sub_RestFields__tree_init(RestResourceWalker_Sub_RestFi
# 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()")
raise RestResourceModelException_ACL("ACL must be a list()")
else:
raise RuntimeError("fields must be contained in a RestResourceBase")
raise RestResourceModelException("fields must be contained in a RestResourceBase")
class RestResourceWalker_Sub_RestResourceBase__tree_init(RestResourceWalker_Sub_RestResourceBase):
@@ -132,8 +141,12 @@ class RestResourceWalker_Sub_RestResourceBase__tree_init(RestResourceWalker_Sub_
# 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)
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 (
@@ -142,7 +155,7 @@ class RestResourceWalker_Sub_RestResourceBase__tree_init(RestResourceWalker_Sub_
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"]
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
@@ -153,7 +166,7 @@ class RestResourceWalker_Sub_RestResourceBase__tree_init(RestResourceWalker_Sub_
# 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()")
raise RestResourceModelException_ACL("ACL must be a list()")
class RestResourceWalker_Root__tree_init(RestResourceWalker_Root):

View File

@@ -15,6 +15,7 @@ from pydantic.fields import FieldInfo
from .rest_types import _T_SupportedRESTFields
from .rest_resource import RestResourceBase
from .rest_exceptions import RestResourceModelException
if TYPE_CHECKING is True:
from typing import Any, Optional
@@ -52,14 +53,14 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
resource_name: str,
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__}")
raise RestResourceModelException(f"Incompatible Field Found: {type(resource).__name__}")
return None
def __init__(
@@ -69,9 +70,9 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
parent: Optional[RestResourceWalker_Sub] = 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.parent: Optional[RestResourceWalker_Sub] = parent
@@ -91,35 +92,10 @@ 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
@@ -163,7 +139,7 @@ class RestResourceWalker_Sub(ABC, Generic[TV_RestResourceWalkerFutureResult]):
elif not isinstance(resource, FieldInfo) and issubclass(resource, RestResourceBase):
_anno = resource
else:
raise RuntimeError("Incompatible resource type")
raise RestResourceModelException("Incompatible resource type")
_datatype = get_args(_anno)
_optional: bool = False
@@ -176,7 +152,7 @@ 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
@@ -232,14 +208,14 @@ class RestResourceWalker_Root:
]
def __init__(self, resource: RestResourceBase | Type[RestResourceBase]) -> None:
self.subwalker_argument: any = 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(
@@ -277,5 +253,4 @@ class RestResourceWalker_Root:
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

@@ -9,12 +9,7 @@ from uuid import UUID
from ipaddress import IPv4Address, IPv4Network
if TYPE_CHECKING is True:
pass
class RestResourceException(Exception):
pass
from .rest_resource import RestResourceBase
T_Gen_DictKeys: type = type({}.keys())
NoneType = type(None)
@@ -63,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")

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

View File

@@ -3,14 +3,10 @@ import unittest
from os import chdir
from pathlib import Path
from typing import Optional
from pydantic import Field
print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
RestResourceHandlerException_Forbiden,
register_rest_rootpoint,
RestResourceBase,
rsrc_verb,
@@ -34,8 +30,8 @@ chdir(testdir_path.parent.resolve())
# to allow mock-ing, all the tested classes are in a function
def init_classes():
class TestResource(RestResourceBase):
username: Optional[str] = Field(None)
secret: Optional[str] = Field(
username: Optional[str] = RestField(None)
secret: Optional[str] = RestField(
None,
exclude=True,
ACL=[
@@ -45,21 +41,21 @@ def init_classes():
)
class TestResource2(RestResourceBase):
version_ro: Optional[str] = Field(
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] = Field("3.2.1")
version: Optional[str] = RestField("3.2.1")
@register_rest_rootpoint
class RootApp(RestResourceBase):
resource_with_secret: TestResource = Field(default=TestResource())
resource_with_secret_ACL: TestResource = Field(
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 = Field(TestResource2())
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
@@ -85,11 +81,11 @@ class Test_RestAPI_ACL(unittest.TestCase):
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(RuntimeError): # TODO: custom exception
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(RuntimeError): # TODO: custom exception
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")
@@ -107,7 +103,7 @@ class Test_RestAPI_ACL(unittest.TestCase):
self.assertEqual(result.get_result(), "null")
self.assertEqual(self.testapp.resource_with_secret.username, None)
with self.assertRaises(RuntimeError): # TODO: custom exception
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)
@@ -122,7 +118,7 @@ class Test_RestAPI_ACL(unittest.TestCase):
self.assertEqual(result.get_result(), '"chacha"')
self.assertEqual(self.testapp.resource_with_secret.username, "chacha")
with self.assertRaises(RuntimeError): # TODO: custom exception
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")
@@ -138,13 +134,13 @@ class Test_RestAPI_ACL(unittest.TestCase):
self.assertEqual(result.get_result(), '"chacha"')
self.assertEqual(self.testapp.resource_with_secret.username, "chacha")
with self.assertRaises(RuntimeError): # TODO: custom exception
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(RuntimeError): # TODO: custom exception
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")
@@ -160,23 +156,23 @@ class Test_RestAPI_ACL(unittest.TestCase):
self.assertEqual(result.get_result(), "null")
self.assertEqual(self.testapp.resource_with_secret_ACL.username, None)
with self.assertRaises(RuntimeError): # TODO: custom exception
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(RuntimeError): # TODO: custom exception
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(RuntimeError): # TODO: custom exception
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(RuntimeError): # TODO: custom exception
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,12 +1,9 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from os import chdir
from pathlib import Path
from typing import Optional, Annotated, ClassVar
from pydantic import Field
from uuid import UUID, uuid4
from time import time, sleep
from typing import Optional, ClassVar
from time import sleep
import uvicorn
import socket
import requests
@@ -14,11 +11,8 @@ from contextlib import closing
from multiprocessing import Process
from requests.adapters import HTTPAdapter
print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
ACL_target_user,
UserLogin,
RestResourceBase,
@@ -38,6 +32,8 @@ from src.pyrestresource import (
)
from test import ThreadedUvicorn
testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
@@ -45,23 +41,39 @@ chdir(testdir_path.parent.resolve())
# to allow mock-ing, all the tested classes are in a function
def init_classes():
user_test = UserLogin(username="TestUser", secret="123456")
user_test2 = UserLogin(username="TestUser2", secret="abcdef")
class TestResource(RestResourceBase):
test_field: Optional[str] = Field("ORIGIN_VALUE")
test_field: Optional[str] = RestField("ORIGIN_VALUE")
class TestResourceACL(RestResourceBase):
test_field: Optional[str] = Field(
test_field: 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_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(RestResourceBaseLogin):
_ar_user_login: ClassVar[list[UserLogin]] = [user_test]
test_resourceACL: TestResource = Field(
_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),
@@ -83,25 +95,120 @@ def find_free_port():
return "localhost", s.getsockname()[1]
def launch_server(ip, port):
init_classes()
uvicorn.run(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True)
class Test_RestAPI_LOGIN_Web(unittest.TestCase):
def setUp(self) -> None:
chdir(testdir_path.parent.resolve())
def test_login_two_users(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:
# login
response = s.put(
f"http://{ip}:{port}/login",
json={"username": "TestUser", "secret": "123456"},
)
self.assertEqual(response.status_code, 201)
# authenticated write (to field)
response = s.put(f"http://{ip}:{port}/test_resource/test_field", json="TEST SET VALUE")
self.assertEqual(response.status_code, 201)
# modified
response = s.get(
f"http://{ip}:{port}/test_resource/test_field",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), "TEST SET VALUE")
# unauthenticated write (to field)
response = s.put(f"http://{ip}:{port}/test_resource/test_field2", json="TEST SET VALUE")
self.assertEqual(response.status_code, 403)
# not modified
response = s.get(
f"http://{ip}:{port}/test_resource/test_field2",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), "ORIGIN_VALUE")
# authenticated write (to field)
response = s.put(f"http://{ip}:{port}/test_resource/test_field_both", json="TEST SET VALUE 2")
self.assertEqual(response.status_code, 201)
# modified
response = s.get(
f"http://{ip}:{port}/test_resource/test_field_both",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), "TEST SET VALUE 2")
# ---------------------------------------
# login 2
response = s.put(
f"http://{ip}:{port}/login",
json={"username": "TestUser2", "secret": "abcdef"},
)
self.assertEqual(response.status_code, 201)
# unauthenticated write (to field)
response = s.put(f"http://{ip}:{port}/test_resource/test_field", json="A TEST SET VALUE")
self.assertEqual(response.status_code, 403)
# not modified
response = s.get(
f"http://{ip}:{port}/test_resource/test_field",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), "TEST SET VALUE")
# authenticated write (to field)
response = s.put(f"http://{ip}:{port}/test_resource/test_field2", json="A TEST SET VALUE")
self.assertEqual(response.status_code, 201)
# modified
response = s.get(
f"http://{ip}:{port}/test_resource/test_field2",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), "A TEST SET VALUE")
# previous (modified) value
response = s.get(
f"http://{ip}:{port}/test_resource/test_field_both",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), "TEST SET VALUE 2")
# authenticated write (to field)
response = s.put(f"http://{ip}:{port}/test_resource/test_field_both", json="A TEST SET VALUE 2")
self.assertEqual(response.status_code, 201)
# modified
response = s.get(
f"http://{ip}:{port}/test_resource/test_field_both",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), "A TEST SET VALUE 2")
finally:
s.close()
server.stop()
def test_login(self):
ip, port = find_free_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))
@@ -143,19 +250,240 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
self.assertEqual(response.json(), "TestUser")
finally:
proc.terminate()
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()
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))
@@ -170,7 +498,7 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
# 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, 500)
self.assertEqual(response.status_code, 403)
# check not modified
response = s.get(
@@ -181,7 +509,7 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
# 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, 500)
self.assertEqual(response.status_code, 403)
# check not modified
response = s.get(
@@ -220,19 +548,15 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
self.assertEqual(response.json(), "TEST SET VALUE 2")
finally:
proc.terminate()
s.close()
server.stop()
def test_access_fieldACL(self):
ip, port = find_free_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))
@@ -247,7 +571,7 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
# 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, 500)
self.assertEqual(response.status_code, 403)
# check not modified
response = s.get(
@@ -258,7 +582,7 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
# 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, 500)
self.assertEqual(response.status_code, 403)
# check not modified
response = s.get(
@@ -297,5 +621,5 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
self.assertEqual(response.json(), "TEST SET VALUE 2")
finally:
proc.terminate()
s.close()
server.stop()

View File

@@ -4,7 +4,6 @@ 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
@@ -14,6 +13,8 @@ print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
RestResourceHandlerException_Forbiden,
register_rest_rootpoint,
RestResourceBase,
rsrc_verb,
@@ -38,19 +39,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
@@ -61,12 +62,12 @@ def init_classes():
Patch_2 = Patch(uuid="d385a1d2-65fa-11ee-8c99-0242ac120002", shortname="testPatch2")
class User(RestResourceBase):
uuid: UUID = Field(
uuid: UUID = RestField(
default_factory=uuid4,
primary_key=True,
)
name: str
secret: str = Field(
secret: str = RestField(
...,
exclude=True,
ACL=[
@@ -82,7 +83,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
@@ -268,11 +269,11 @@ class Test_RestAPI_GET(unittest.TestCase):
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,
@@ -302,7 +303,7 @@ class Test_RestAPI_PUT(unittest.TestCase):
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/uuid",
rsrc_verb.PUT,
@@ -483,7 +484,7 @@ 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

View File

@@ -3,9 +3,9 @@ import unittest
from os import chdir
from pathlib import Path
from typing import Annotated
from pydantic import Field
from src.pyrestresource import (
RestField,
register_rest_rootpoint,
RestResourceBase,
rsrc_verb,
@@ -35,33 +35,32 @@ def init_classes():
class ResourcePlugin_Info(ResourcePlugin_RestResourceBase_default):
def handle_resource_get(self, resource: Info_get, params: RestRequestParams_GET) -> Info_get:
print("HOOK GET !!")
return Info_get(version="65.45", api_version="98.321")
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
@@ -76,11 +75,11 @@ def init_bad_plugin1():
...
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"))
test: TestResource = RestField(default=TestResource(tetvaluestr="testvalue"))
RootApp2()
@@ -95,9 +94,10 @@ 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.get_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.get_result())
self.assertEqual(result.get_result(), '"42"')
def test_put_field_version_resourceplugin(self):

View File

@@ -5,7 +5,6 @@ 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
@@ -13,6 +12,7 @@ print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
RestResourceBase,
)
@@ -80,7 +80,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

@@ -5,13 +5,13 @@ from typing import Optional
from os import chdir
from pathlib import Path
from pydantic import Field
print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
RestResourceBase,
)
@@ -80,7 +80,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

@@ -4,7 +4,6 @@ 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
@@ -14,11 +13,13 @@ import requests
from contextlib import closing
from multiprocessing import Process
from requests.adapters import HTTPAdapter
import coverage
print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
register_rest_rootpoint,
RestResourceBase,
rsrc_verb,
@@ -29,6 +30,8 @@ from src.pyrestresource import (
)
from pprint import pprint
from test import ThreadedUvicorn
testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
@@ -40,19 +43,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 +66,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 +77,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
@@ -120,25 +123,16 @@ def find_free_port():
return "localhost", s.getsockname()[1]
def launch_server(ip, port):
init_classes()
uvicorn.run(f"{__loader__.name}:RootApp", port=port, host="0.0.0.0", log_level="warning", factory=True)
class Test_RestAPI_WebServer(unittest.TestCase):
def setUp(self) -> None:
chdir(testdir_path.parent.resolve())
def test_nomal_AllCmd_games(self):
ip, port = find_free_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))
@@ -154,15 +148,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",
@@ -281,23 +266,19 @@ 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()
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))
@@ -375,5 +356,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()