Compare commits
10 Commits
0.0.1.post
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 27c576db21 | |||
|
|
6311d90a2d | ||
|
|
7e13d49feb | ||
|
|
9b3e847908 | ||
|
|
3afebdba33 | ||
|
|
0ec875e497 | ||
|
|
d0e146ac76 | ||
|
|
4af812cf80 | ||
|
|
d58173f07b | ||
|
|
04ef407a6f |
14
Jenkinsfile
vendored
14
Jenkinsfile
vendored
@@ -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",
|
||||
|
||||
16
README.md
16
README.md
@@ -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/).
|
||||
@@ -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
17
RUN_types.launch
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__"
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ class RestResourceException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceConfigException(RestResourceException):
|
||||
pass
|
||||
|
||||
|
||||
class RestResourceModelException(RestResourceException):
|
||||
pass
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
103
src/pyrestresource/rest_model.py
Normal file
103
src/pyrestresource/rest_model.py
Normal 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,
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
23
test/ThreadedUvicorn.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user