10 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
28 changed files with 426 additions and 244 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,7 +34,7 @@ classifiers = [
]
dependencies = [
'packaging',
'typegard',
'typeguard',
'pydantic>=2.4,<3',
'uvicorn>=0.23'
]
@@ -48,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,22 +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,
)
from .rest_request_opt import (
RestRequestParams_POST,
RestRequestParams_DELETE,
@@ -45,18 +32,17 @@ 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,
)
from .rest_ACL import ACL_target_user, ACL_target_group, ACL_target_group_Any, ACL_record, ACL_rule
from .rest_resource import RestResourceBase
from .rest_login import (
RestResourceBaseLogin,
UserLogin,
)
from .rest_exceptions import (
RestResourceException,
RestResourceLoginException,
@@ -67,3 +53,13 @@ from .rest_exceptions import (
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

@@ -19,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:

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

@@ -2,6 +2,10 @@ class RestResourceException(Exception):
pass
class RestResourceConfigException(RestResourceException):
pass
class RestResourceModelException(RestResourceException):
pass

View File

@@ -16,10 +16,11 @@ from typing import Optional, ClassVar, TYPE_CHECKING
from secrets import token_hex, compare_digest
from datetime import datetime, timedelta
from pydantic import BaseModel, Field
from 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 (
@@ -30,7 +31,8 @@ from .rest_exceptions import (
)
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):
@@ -41,24 +43,26 @@ class UserLogin(BaseModel):
class UserSession(BaseModel):
last_update: datetime
user_login: UserLogin
client: Optional[tuple[str, int]]
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=[
@@ -73,7 +77,7 @@ class RestResourceBaseLogin(RestResourceBase):
_ar_user_session: dict[str, UserSession] = {}
_max_session_inactive: ClassVar[timedelta] = timedelta(minutes=20)
_max_session_time: ClassVar[timedelta] = timedelta(hours=12)
login: Login = Field(default=Login(), plugin=ResourcePlugin_Login)
login: Login = RestField(default=Login(), plugin=ResourcePlugin_Login)
def get_new_cookie_expiration_date(self) -> datetime:
return datetime.now() + self._max_session_time
@@ -120,8 +124,7 @@ class RestResourceBaseLogin(RestResourceBase):
pass
pass
if already_failed:
raise RestResourceLoginException_InvalidCredentials()
raise RestResourceLoginException_InvalidCredentials()
def _register_user_session(self, user_login: UserLogin, request: RestRequest) -> str:
token = token_hex(16)

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,6 +28,12 @@ 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
@@ -121,7 +128,7 @@ class RestRequest(Generic[_T_RestRequestParams]):
self.verb: rsrc_verb
self.data: dict
self._raw_headers: list[Any] = []
self._client: tuple[str, int] = ()
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()
@@ -179,7 +186,7 @@ class RestRequest(Generic[_T_RestRequestParams]):
def set_client(self, client: tuple[str, int]) -> None:
self._client = client
def get_client(self) -> tuple[str, int]:
def get_client(self) -> tuple[str, int] | tuple[()]:
return self._client
def set_headers(self, headers: list[Any]) -> None:
@@ -193,11 +200,18 @@ class RestRequest(Generic[_T_RestRequestParams]):
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 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"
@@ -205,7 +219,7 @@ class RestRequest(Generic[_T_RestRequestParams]):
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:
def get_host(self) -> str | dict[str, str | None] | None:
return self.headers["host"]
def set_result(self, result: str):

View File

@@ -12,8 +12,8 @@ import pprint
from pydantic import BaseModel
from .rest_types import rsrc_verb
from .helpers import _JSONEncoder, forward_exception
from .rest_types import rsrc_verb, _T_SupportedRESTFields
from .rest_ACL import (
ACL_record,
@@ -23,7 +23,6 @@ from .rest_ACL import (
ACL_rule,
)
from .rest_request import RestRequest
from .rest_exceptions import (
RestResourceLoginException_InvalidSession,
RestResourceLoginException_SessionTimeout,
@@ -37,6 +36,9 @@ from .rest_exceptions import (
)
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,
@@ -44,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]] = {}
@@ -52,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],
]
] = {}
@@ -92,15 +93,16 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
"""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 RestResourceException("Incompatible verb")
@@ -122,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"]
@@ -148,7 +150,7 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
assert request is not None
header_resp = {
header_resp: dict[str, Any] = {
"type": "http.response.start",
"status": request.get_status(),
"headers": [
@@ -162,8 +164,9 @@ class RestResourceBase(ABC, BaseModel, validate_assignment=True):
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(
{

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 (
@@ -100,8 +102,8 @@ class ResourceHandler(
elif None in [url, verb]:
raise RestResourceHandlerException("if req not set, url,verb must be setted")
else:
if url is None or verb is None:
raise RestResourceHandlerException("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(resource, url, verb, data, query_string)
@@ -317,11 +319,16 @@ class ResourceHandler_dict(
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)
@@ -479,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.req.get_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(
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)
@@ -512,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,
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.req.get_root_resource()),
)
value = plugin_rsrc.handle_resource_get(value, params)
value = plugin_resource.handle_resource_get(value, params)
return value
@@ -551,18 +559,19 @@ class ResourceHandler_RestResourceBase(
# 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,
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)

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

@@ -10,10 +10,11 @@ from .rest_types import (
TV_SupportedRESTFields,
TV_RestResourceBase,
)
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,
@@ -31,9 +32,13 @@ if TYPE_CHECKING is True:
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:
@@ -46,6 +51,10 @@ class ResourcePlugin(ABC):
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:

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
@@ -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
@@ -105,7 +112,7 @@ class RestResourceWalker_Sub_RestFields__tree_init(RestResourceWalker_Sub_RestFi
]
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
@@ -134,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 (
@@ -144,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

View File

@@ -53,7 +53,7 @@ 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)
@@ -70,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
@@ -208,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(
@@ -254,4 +254,3 @@ class RestResourceWalker_Root:
return sub_walker_initial.chain_process_future()
else:
raise RestResourceModelException("Invalid Rootpoint")
return None

View File

@@ -9,8 +9,7 @@ from uuid import UUID
from ipaddress import IPv4Address, IPv4Network
if TYPE_CHECKING is True:
pass
from .rest_resource import RestResourceBase
T_Gen_DictKeys: type = type({}.keys())
NoneType = type(None)

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,9 +3,9 @@ import unittest
from os import chdir
from pathlib import Path
from typing import Optional
from pydantic import Field
from src.pyrestresource import (
RestField,
RestResourceHandlerException_Forbiden,
register_rest_rootpoint,
RestResourceBase,
@@ -30,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=[
@@ -41,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

View File

@@ -3,7 +3,6 @@ import unittest
from os import chdir
from pathlib import Path
from typing import Optional, ClassVar
from pydantic import Field
from time import sleep
import uvicorn
import socket
@@ -13,6 +12,7 @@ from multiprocessing import Process
from requests.adapters import HTTPAdapter
from src.pyrestresource import (
RestField,
ACL_target_user,
UserLogin,
RestResourceBase,
@@ -32,6 +32,8 @@ from src.pyrestresource import (
)
from test import ThreadedUvicorn
testdir_path = Path(__file__).parent.resolve()
chdir(testdir_path.parent.resolve())
@@ -42,24 +44,24 @@ def init_classes():
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] = Field(
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] = Field(
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),
@@ -71,7 +73,7 @@ def init_classes():
@register_rest_rootpoint
class RootApp(RestResourceBaseLogin):
_ar_user_login: ClassVar[list[UserLogin]] = [user_test, user_test2]
test_resourceACL: TestResource = Field(
test_resourceACL: TestResource = RestField(
TestResource(),
ACL=[
ACL_record(verbs=[rsrc_verb.PUT], target=ACL_target_user(name=user_test.username), rule=ACL_rule.ALLOW),
@@ -93,26 +95,18 @@ 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()
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))
@@ -206,19 +200,15 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
self.assertEqual(response.json(), "A TEST SET VALUE 2")
finally:
proc.terminate()
s.close()
server.stop()
def test_login(self):
ip, port = find_free_port()
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))
@@ -260,19 +250,15 @@ 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()
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)
s1 = requests.Session()
s1.mount("http://", HTTPAdapter(max_retries=0))
@@ -378,20 +364,16 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
self.assertEqual(response.json(), "__ANNONYMOUS__")
finally:
proc.terminate()
s1.close()
s2.close()
server.stop()
def test_login_wrong_pwd(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))
@@ -493,19 +475,15 @@ class Test_RestAPI_LOGIN_Web(unittest.TestCase):
self.assertDictEqual(s.cookies.get_dict(), {})
finally:
proc.terminate()
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))
@@ -570,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))
@@ -647,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,7 @@ print(__name__)
print(__package__)
from src.pyrestresource import (
RestField,
RestResourceHandlerException_Forbiden,
register_rest_rootpoint,
RestResourceBase,
@@ -39,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
@@ -62,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=[
@@ -83,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
@@ -484,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,
@@ -40,27 +40,27 @@ def init_classes():
class Info_get(RestResourceBase):
# test plugin injection within annotation
# + test plugin on a simple field
version: Annotated[str, Field(plugin=ResourcePlugin_version_get)]
version: Annotated[str, RestField(plugin=ResourcePlugin_version_get)]
api_version: str
class Info_put(RestResourceBase):
# test plugin injection within annotation
# + test plugin on a simple field
version: Annotated[str, Field(plugin=ResourcePlugin_version_put)]
version: Annotated[str, RestField(plugin=ResourcePlugin_version_put)]
api_version: str
@register_rest_rootpoint
class RootApp(RestResourceBase):
# test plugin injection within Field value
# + test plugin on a RestResourceBase field
info: Info_get = Field(
info: Info_get = RestField(
default=Info_get(version="0.0.1", api_version="0.0.2"),
plugin=ResourcePlugin_Info,
)
info_put: Info_put = Field(
info_put: Info_put = RestField(
default=Info_put(version="0.0.1", api_version="0.0.2"),
)
info2: Info_get = Field(default=Info_get(version="0.0.2", api_version="0.0.3"))
info2: Info_get = RestField(default=Info_get(version="0.0.2", api_version="0.0.3"))
# this add the classes to globals to allow using them later on
# => this is only for uinit-testing purpose and is not needed in real use
@@ -75,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()

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))
@@ -272,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))
@@ -366,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()