mirror of
https://github.com/dkmstr/openuds.git
synced 2025-10-07 15:33:51 +03:00
Add REST API information to various handlers and models
- Introduced REST_API_INFO class variable to Handler and various ModelHandler subclasses to provide metadata for auto-generated APIs. - Updated api_helpers to utilize REST_API_INFO for dynamic naming and descriptions. - Enhanced API response generation functions to include OData parameters and improved request body descriptions. - Added checks in UserServiceManager to prevent actions on already removed services. - Cleaned up code formatting and comments for better readability.
This commit is contained in:
@@ -72,6 +72,8 @@ class Handler(abc.ABC):
|
||||
|
||||
ROLE: typing.ClassVar[consts.UserRole] = consts.UserRole.USER # By default, only users can access
|
||||
|
||||
REST_API_INFO: typing.ClassVar[types.rest.api.RestApiInfo] = types.rest.api.RestApiInfo()
|
||||
|
||||
_request: 'ExtendedHttpRequestWithUser' # It's a modified HttpRequest
|
||||
_path: str
|
||||
_operation: str
|
||||
|
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Enclosed methods under /item path
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AccountItem(types.rest.BaseRestItem):
|
||||
id: str
|
||||
@@ -84,6 +85,11 @@ class Accounts(ModelHandler[AccountItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.UNTYPED,
|
||||
)
|
||||
|
||||
def get_item(self, item: 'models.Model') -> AccountItem:
|
||||
item = ensure.is_instance(item, Account)
|
||||
return AccountItem(
|
||||
|
@@ -91,7 +91,7 @@ class AuthenticatorItem(types.rest.ManagedObjectItem[Authenticator]):
|
||||
users_count: int
|
||||
permission: int
|
||||
|
||||
type_info: types.rest.TypeInfo|None
|
||||
type_info: types.rest.TypeInfo | None
|
||||
|
||||
|
||||
# Enclosed methods under /auth path
|
||||
@@ -119,6 +119,11 @@ class Authenticators(ModelHandler[AuthenticatorItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.TYPED,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def possible_types(cls: type[typing.Self]) -> collections.abc.Iterable[type[auths.Authenticator]]:
|
||||
return auths.factory().providers().values()
|
||||
|
@@ -46,7 +46,6 @@ from uds.REST.model import ModelHandler
|
||||
from .calendarrules import CalendarRules
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -62,6 +61,7 @@ class CalendarItem(types.rest.BaseRestItem):
|
||||
number_actions: int
|
||||
permission: types.permissions.PermissionType
|
||||
|
||||
|
||||
class Calendars(ModelHandler[CalendarItem]):
|
||||
"""
|
||||
Processes REST requests about calendars
|
||||
@@ -84,6 +84,11 @@ class Calendars(ModelHandler[CalendarItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.UNTYPED,
|
||||
)
|
||||
|
||||
def get_item(self, item: 'models.Model') -> CalendarItem:
|
||||
item = ensure.is_instance(item, Calendar)
|
||||
return CalendarItem(
|
||||
|
@@ -91,6 +91,11 @@ class Images(ModelHandler[ImageItem]):
|
||||
# This has no get_gui because its treated on the admin or client.
|
||||
# We expect an Image List
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.NONE,
|
||||
)
|
||||
|
||||
def get_item(self, item: 'models.Model') -> ImageItem:
|
||||
item = ensure.is_instance(item, Image)
|
||||
return ImageItem(
|
||||
|
@@ -132,6 +132,11 @@ class MetaPools(ModelHandler[MetaPoolItem]):
|
||||
types.rest.ModelCustomMethod('get_fallback_access', True),
|
||||
]
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.UNTYPED,
|
||||
)
|
||||
|
||||
def get_item(self, item: 'models.Model') -> MetaPoolItem:
|
||||
item = ensure.is_instance(item, MetaPool)
|
||||
# if item does not have an associated service, hide it (the case, for example, for a removed service)
|
||||
|
@@ -78,6 +78,11 @@ class MFA(ModelHandler[MFAItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.TYPED,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def possible_types(cls: type[typing.Self]) -> collections.abc.Iterable[type[mfas.MFA]]:
|
||||
return mfas.factory().providers().values()
|
||||
|
@@ -47,6 +47,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Enclosed methods under /item path
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class NetworkItem(types.rest.BaseRestItem):
|
||||
id: str
|
||||
@@ -77,6 +78,11 @@ class Networks(ModelHandler[NetworkItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.UNTYPED,
|
||||
)
|
||||
|
||||
def get_gui(self, for_type: str) -> list[types.ui.GuiElement]:
|
||||
return (
|
||||
ui_utils.GuiBuilder()
|
||||
|
@@ -87,6 +87,11 @@ class Notifiers(ModelHandler[NotifierItem]):
|
||||
.text_column(name='tags', title=_('Tags'), visible=False)
|
||||
).build()
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.TYPED,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def possible_types(cls: type[typing.Self]) -> collections.abc.Iterable[type[messaging.Notifier]]:
|
||||
return messaging.factory().providers().values()
|
||||
|
@@ -77,6 +77,11 @@ class OsManagers(ModelHandler[OsManagerItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.TYPED,
|
||||
)
|
||||
|
||||
def os_manager_as_dict(self, item: OSManager) -> OsManagerItem:
|
||||
type_ = item.get_type()
|
||||
ret_value = OsManagerItem(
|
||||
|
@@ -52,7 +52,6 @@ from .services_usage import ServicesUsage
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
# Helper class for Provider offers
|
||||
@dataclasses.dataclass
|
||||
class OfferItem(types.rest.BaseRestItem):
|
||||
@@ -99,6 +98,11 @@ class Providers(ModelHandler[ProviderItem]):
|
||||
.row_style(prefix='row-maintenance-', field='maintenance_mode')
|
||||
).build()
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.TYPED,
|
||||
)
|
||||
|
||||
def get_item(self, item: 'Model') -> ProviderItem:
|
||||
item = ensure.is_instance(item, Provider)
|
||||
type_ = item.get_type()
|
||||
|
@@ -88,6 +88,11 @@ class Reports(model.BaseModelHandler[ReportItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.TYPED,
|
||||
)
|
||||
|
||||
def _locate_report(
|
||||
self, uuid: str, values: typing.Optional[typing.Dict[str, typing.Any]] = None
|
||||
) -> 'Report':
|
||||
|
@@ -143,6 +143,11 @@ class ServersServers(DetailHandler[ServerItem]):
|
||||
|
||||
CUSTOM_METHODS = ['maintenance', 'importcsv']
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.UNTYPED,
|
||||
)
|
||||
|
||||
def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult[ServerItem]:
|
||||
parent = typing.cast('models.ServerGroup', parent) # We will receive for sure
|
||||
try:
|
||||
@@ -197,7 +202,7 @@ class ServersServers(DetailHandler[ServerItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
def get_gui(self, parent: 'Model', for_type: str = '') -> list[types.ui.GuiElement]:
|
||||
def get_gui(self, parent: 'Model', for_type: str) -> list[types.ui.GuiElement]:
|
||||
parent = ensure.is_instance(parent, models.ServerGroup)
|
||||
kind, subkind = parent.server_type, parent.subtype
|
||||
title = _('of type') + f' {subkind.upper()} {kind.name.capitalize()}'
|
||||
@@ -456,6 +461,11 @@ class ServersGroups(ModelHandler[GroupItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.TYPED,
|
||||
)
|
||||
|
||||
def enum_types(
|
||||
self, *args: typing.Any, **kwargs: typing.Any
|
||||
) -> typing.Generator[types.rest.TypeInfo, None, None]:
|
||||
|
@@ -104,6 +104,11 @@ class Services(DetailHandler[ServiceItem]): # pylint: disable=too-many-public-m
|
||||
|
||||
CUSTOM_METHODS = ['servicepools']
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.TYPED,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def service_info(item: models.Service) -> ServiceInfo:
|
||||
info = item.get_type()
|
||||
|
@@ -74,6 +74,11 @@ class ServicesPoolGroups(ModelHandler[ServicePoolGroupItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.UNTYPED,
|
||||
)
|
||||
|
||||
def pre_save(self, fields: dict[str, typing.Any]) -> None:
|
||||
img_id = fields['image_id']
|
||||
fields['image_id'] = None
|
||||
|
@@ -170,6 +170,11 @@ class ServicesPools(ModelHandler[ServicePoolItem]):
|
||||
types.rest.ModelCustomMethod('add_log', True),
|
||||
]
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.UNTYPED,
|
||||
)
|
||||
|
||||
def get_items(
|
||||
self, *args: typing.Any, **kwargs: typing.Any
|
||||
) -> typing.Generator[ServicePoolItem, None, None]:
|
||||
|
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Enclosed methods under /item path
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TransportItem(types.rest.ManagedObjectItem[Transport]):
|
||||
id: str
|
||||
@@ -92,6 +93,11 @@ class Transports(ModelHandler[TransportItem]):
|
||||
.text_column(name='tags', title=_('tags'), visible=False)
|
||||
).build()
|
||||
|
||||
# Rest api related information to complete the auto-generated API
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.TYPED,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def possible_types(cls: type[typing.Self]) -> collections.abc.Iterable[type[transports.Transport]]:
|
||||
return transports.factory().providers().values()
|
||||
|
@@ -47,6 +47,7 @@ from uds.REST.model import DetailHandler, ModelHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TunnelServerItem(types.rest.BaseRestItem):
|
||||
id: str
|
||||
@@ -57,9 +58,12 @@ class TunnelServerItem(types.rest.BaseRestItem):
|
||||
|
||||
|
||||
class TunnelServers(DetailHandler[TunnelServerItem]):
|
||||
# tunnels/[id]/servers
|
||||
CUSTOM_METHODS = ['maintenance']
|
||||
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
name='TunnelServers', description='Tunnel servers assigned to a tunnel'
|
||||
)
|
||||
|
||||
def get_items(
|
||||
self, parent: 'Model', item: typing.Optional[str]
|
||||
) -> types.rest.ItemsResult[TunnelServerItem]:
|
||||
@@ -134,6 +138,7 @@ class TunnelServers(DetailHandler[TunnelServerItem]):
|
||||
item.save()
|
||||
return 'ok'
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TunnelItem(types.rest.BaseRestItem):
|
||||
id: str
|
||||
@@ -173,6 +178,12 @@ class Tunnels(ModelHandler[TunnelItem]):
|
||||
.build()
|
||||
)
|
||||
|
||||
REST_API_INFO = types.rest.api.RestApiInfo(
|
||||
name='Tunnels',
|
||||
description='Tunnel management',
|
||||
gui_type=types.rest.api.RestApiInfoGuiType.UNTYPED,
|
||||
)
|
||||
|
||||
def get_gui(self, for_type: str) -> list[types.ui.GuiElement]:
|
||||
return (
|
||||
ui_utils.GuiBuilder()
|
||||
|
@@ -58,7 +58,7 @@ def api_paths(
|
||||
Returns the API operations that should be registered
|
||||
"""
|
||||
|
||||
name = path.split('/')[-1]
|
||||
name = cls.REST_API_INFO.name if cls.REST_API_INFO.name else path.split('/')[-1].capitalize()
|
||||
get_tags = tags
|
||||
put_tags = tags # + ['Create', 'Modify']
|
||||
# post_tags = tags + ['Create']
|
||||
@@ -71,12 +71,12 @@ def api_paths(
|
||||
else:
|
||||
base_type_name = base_type.__name__
|
||||
# TODO: Append "custom" methods
|
||||
return {
|
||||
api_desc = {
|
||||
path: types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get all {name} items',
|
||||
description=f'Retrieve a list of all {name} items',
|
||||
parameters=[],
|
||||
parameters=api_utils.gen_odata_parameters(),
|
||||
responses=api_utils.gen_response(base_type_name, single=False),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
@@ -94,15 +94,7 @@ def api_paths(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get {name} item by UUID',
|
||||
description=f'Retrieve a {name} item by UUID',
|
||||
parameters=[
|
||||
types.rest.api.Parameter(
|
||||
name='uuid',
|
||||
in_='path',
|
||||
required=True,
|
||||
description='The UUID of the item',
|
||||
schema=types.rest.api.Schema(type='string', format='uuid'),
|
||||
)
|
||||
],
|
||||
parameters=api_utils.gen_uuid_parameters(with_odata=True),
|
||||
responses=api_utils.gen_response(base_type_name, with_404=True),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
@@ -110,15 +102,7 @@ def api_paths(
|
||||
put=types.rest.api.Operation(
|
||||
summary=f'Update {name} item by UUID',
|
||||
description=f'Update an existing {name} item by UUID',
|
||||
parameters=[
|
||||
types.rest.api.Parameter(
|
||||
name='uuid',
|
||||
in_='path',
|
||||
required=True,
|
||||
description='The UUID of the item',
|
||||
schema=types.rest.api.Schema(type='string', format='uuid'),
|
||||
)
|
||||
],
|
||||
parameters=api_utils.gen_uuid_parameters(with_odata=True),
|
||||
responses=api_utils.gen_response(base_type_name, with_404=True),
|
||||
tags=put_tags,
|
||||
security=security,
|
||||
@@ -126,15 +110,7 @@ def api_paths(
|
||||
delete=types.rest.api.Operation(
|
||||
summary=f'Delete {name} item by UUID',
|
||||
description=f'Delete a {name} item by UUID',
|
||||
parameters=[
|
||||
types.rest.api.Parameter(
|
||||
name='uuid',
|
||||
in_='path',
|
||||
required=True,
|
||||
description='The UUID of the item',
|
||||
schema=types.rest.api.Schema(type='string', format='uuid'),
|
||||
)
|
||||
],
|
||||
parameters=api_utils.gen_uuid_parameters(with_odata=True),
|
||||
responses=api_utils.gen_response(base_type_name, with_404=True),
|
||||
tags=delete_tags,
|
||||
security=security,
|
||||
@@ -144,7 +120,7 @@ def api_paths(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get overview of {name} items',
|
||||
description=f'Retrieve an overview of {name} items',
|
||||
parameters=[],
|
||||
parameters=api_utils.gen_odata_parameters(),
|
||||
responses=api_utils.gen_response(base_type_name, single=False),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
@@ -188,32 +164,38 @@ def api_paths(
|
||||
security=security,
|
||||
)
|
||||
),
|
||||
f'{path}/{consts.rest.GUI}': types.rest.api.PathItem(
|
||||
}
|
||||
|
||||
if cls.REST_API_INFO.gui_type.is_untyped():
|
||||
api_desc[f'{path}/{consts.rest.GUI}'] = types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get GUI representation of {name} items',
|
||||
description=f'Retrieve the GUI representation of {name} items',
|
||||
parameters=[],
|
||||
responses=api_utils.gen_response('GuiElement', with_404=True),
|
||||
responses=api_utils.gen_response('GuiElement', single=False, with_404=True),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
)
|
||||
),
|
||||
f'{path}/{consts.rest.GUI}/{{type}}': types.rest.api.PathItem(
|
||||
)
|
||||
|
||||
if cls.REST_API_INFO.gui_type.is_typed():
|
||||
api_desc[f'{path}/{consts.rest.GUI}/{{type}}'] = types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get {name} item by type',
|
||||
description=f'Retrieve a {name} item by type',
|
||||
summary=f'Get GUI representation of {name} type',
|
||||
description=f'Retrieve a {name} GUI representation by type',
|
||||
parameters=[
|
||||
types.rest.api.Parameter(
|
||||
name='type',
|
||||
in_='path',
|
||||
required=True,
|
||||
description='The type of the item',
|
||||
description=f'The type of the {name} GUI representation',
|
||||
schema=types.rest.api.Schema(type='string'),
|
||||
)
|
||||
],
|
||||
responses=api_utils.gen_response('GuiElement', with_404=True),
|
||||
responses=api_utils.gen_response('GuiElement', single=False, with_404=True),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return api_desc
|
||||
|
@@ -85,16 +85,15 @@ class ModelHandler(BaseModelHandler[types.rest.T_Item], abc.ABC):
|
||||
# Same, but for exclude
|
||||
EXCLUDE: 'typing.ClassVar[typing.Optional[collections.abc.Mapping[str, typing.Any]]]' = None
|
||||
|
||||
# If this model respond to "custom" methods, we will declare them here
|
||||
# This is an array of tuples of two items, where first is method and second inticates if method needs parent id (normal behavior is it needs it)
|
||||
# For example ('services', True) -- > .../id_parent/services
|
||||
# ('services', False) --> ..../services
|
||||
CUSTOM_METHODS: typing.ClassVar[list[types.rest.ModelCustomMethod]] = (
|
||||
[]
|
||||
) # If this model respond to "custom" methods, we will declare them here
|
||||
CUSTOM_METHODS: typing.ClassVar[list[types.rest.ModelCustomMethod]] = []
|
||||
|
||||
# If this model has details, which ones
|
||||
DETAIL: typing.ClassVar[typing.Optional[dict[str, type['DetailHandler[typing.Any]']]]] = (
|
||||
None # Dictionary containing detail routing
|
||||
)
|
||||
# Dictionary containing detail routing
|
||||
DETAIL: typing.ClassVar[typing.Optional[dict[str, type['DetailHandler[typing.Any]']]]] = None
|
||||
# Fields that are going to be saved directly
|
||||
# * If a field is in the form "field:default" and field is not present in the request, default will be used
|
||||
# * If the "default" is the string "None", then the default will be None
|
||||
@@ -268,13 +267,6 @@ class ModelHandler(BaseModelHandler[types.rest.T_Item], abc.ABC):
|
||||
# logger.exception('Exception getting item from {0}'.format(self.model))
|
||||
|
||||
def get(self) -> typing.Any:
|
||||
"""
|
||||
Wraps real get method so we can process filters if they exists
|
||||
"""
|
||||
return self.process_get()
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def process_get(self) -> typing.Any:
|
||||
logger.debug('method GET for %s, %s', self.__class__.__name__, self._args)
|
||||
number_of_args = len(self._args)
|
||||
|
||||
|
@@ -56,12 +56,12 @@ def api_paths(
|
||||
"""
|
||||
Returns the API operations that should be registered
|
||||
"""
|
||||
# The the base pathº
|
||||
cls_model = cls.MODEL.__name__
|
||||
|
||||
name = cls.REST_API_INFO.name if cls.REST_API_INFO.name else cls.MODEL.__name__
|
||||
get_tags = tags
|
||||
put_tags = tags # + ['Create', 'Modify']
|
||||
put_tags = tags # + ['Create', 'Modify']
|
||||
# post_tags = tags + ['Create']
|
||||
delete_tags = tags # + ['Delete']
|
||||
delete_tags = tags # + ['Delete']
|
||||
|
||||
base_type = next(iter(api_utils.get_generic_types(cls)), None)
|
||||
if base_type is None:
|
||||
@@ -70,20 +70,21 @@ def api_paths(
|
||||
else:
|
||||
base_type_name = base_type.__name__
|
||||
|
||||
return {
|
||||
api_desc = {
|
||||
path: types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get all {cls_model} items',
|
||||
description=f'Retrieve a list of all {cls_model} items',
|
||||
parameters=[],
|
||||
summary=f'Get all {name} items',
|
||||
description=f'Retrieve a list of all {name} items',
|
||||
parameters=api_utils.gen_odata_parameters(),
|
||||
responses=api_utils.gen_response(base_type_name, single=False),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
),
|
||||
put=types.rest.api.Operation(
|
||||
summary=f'Creates a new {cls_model} item',
|
||||
description=f'Creates a new, nonexisting {cls_model} item',
|
||||
summary=f'Creates a new {name} item',
|
||||
description=f'Creates a new, nonexisting {name} item',
|
||||
parameters=[],
|
||||
requestBody=api_utils.gen_request_body(base_type_name, create=True),
|
||||
responses=api_utils.gen_response(base_type_name, with_404=True),
|
||||
tags=put_tags,
|
||||
security=security,
|
||||
@@ -91,49 +92,25 @@ def api_paths(
|
||||
),
|
||||
f'{path}/{{uuid}}': types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get {cls_model} item by UUID',
|
||||
description=f'Retrieve a {cls_model} item by UUID',
|
||||
parameters=[
|
||||
types.rest.api.Parameter(
|
||||
name='uuid',
|
||||
in_='path',
|
||||
required=True,
|
||||
description='The UUID of the item',
|
||||
schema=types.rest.api.Schema(type='string', format='uuid'),
|
||||
)
|
||||
],
|
||||
summary=f'Get {name} item by UUID',
|
||||
description=f'Retrieve a {name} item by UUID',
|
||||
parameters=api_utils.gen_uuid_parameters(with_odata=True),
|
||||
responses=api_utils.gen_response(base_type_name, with_404=True),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
),
|
||||
put=types.rest.api.Operation(
|
||||
summary=f'Update {cls_model} item by UUID',
|
||||
description=f'Update an existing {cls_model} item by UUID',
|
||||
parameters=[
|
||||
types.rest.api.Parameter(
|
||||
name='uuid',
|
||||
in_='path',
|
||||
required=True,
|
||||
description='The UUID of the item',
|
||||
schema=types.rest.api.Schema(type='string', format='uuid'),
|
||||
)
|
||||
],
|
||||
summary=f'Update {name} item by UUID',
|
||||
description=f'Update an existing {name} item by UUID',
|
||||
parameters=api_utils.gen_uuid_parameters(with_odata=False),
|
||||
responses=api_utils.gen_response(base_type_name, with_404=True),
|
||||
tags=put_tags,
|
||||
security=security,
|
||||
),
|
||||
delete=types.rest.api.Operation(
|
||||
summary=f'Delete {cls_model} item by UUID',
|
||||
description=f'Delete a {cls_model} item by UUID',
|
||||
parameters=[
|
||||
types.rest.api.Parameter(
|
||||
name='uuid',
|
||||
in_='path',
|
||||
required=True,
|
||||
description='The UUID of the item',
|
||||
schema=types.rest.api.Schema(type='string', format='uuid'),
|
||||
)
|
||||
],
|
||||
summary=f'Delete {name} item by UUID',
|
||||
description=f'Delete a {name} item by UUID',
|
||||
parameters=api_utils.gen_uuid_parameters(with_odata=False),
|
||||
responses=api_utils.gen_response(base_type_name, with_404=True),
|
||||
tags=delete_tags,
|
||||
security=security,
|
||||
@@ -141,9 +118,9 @@ def api_paths(
|
||||
),
|
||||
f'{path}/{consts.rest.OVERVIEW}': types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get overview of {cls_model} items',
|
||||
description=f'Retrieve an overview of {cls_model} items',
|
||||
parameters=[],
|
||||
summary=f'Get overview of {name} items',
|
||||
description=f'Retrieve an overview of {name} items',
|
||||
parameters=api_utils.gen_odata_parameters(),
|
||||
responses=api_utils.gen_response(base_type_name, single=False),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
@@ -151,8 +128,8 @@ def api_paths(
|
||||
),
|
||||
f'{path}/{consts.rest.TABLEINFO}': types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get table info of {cls_model} items',
|
||||
description=f'Retrieve table info of {cls_model} items',
|
||||
summary=f'Get table info of {name} items',
|
||||
description=f'Retrieve table info of {name} items',
|
||||
parameters=[],
|
||||
responses=api_utils.gen_response('TableInfo', with_404=True),
|
||||
tags=get_tags,
|
||||
@@ -161,8 +138,8 @@ def api_paths(
|
||||
),
|
||||
f'{path}/{consts.rest.TYPES}': types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get types of {cls_model} items',
|
||||
description=f'Retrieve types of {cls_model} items',
|
||||
summary=f'Get types of {name} items',
|
||||
description=f'Retrieve types of {name} items',
|
||||
parameters=[],
|
||||
responses=api_utils.gen_response(base_type_name, single=False),
|
||||
tags=get_tags,
|
||||
@@ -171,8 +148,8 @@ def api_paths(
|
||||
),
|
||||
f'{path}/{consts.rest.TYPES}/{{type}}': types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get {cls_model} item by type',
|
||||
description=f'Retrieve a {cls_model} item by type',
|
||||
summary=f'Get {name} item by type',
|
||||
description=f'Retrieve a {name} item by type',
|
||||
parameters=[
|
||||
types.rest.api.Parameter(
|
||||
name='type',
|
||||
@@ -187,33 +164,37 @@ def api_paths(
|
||||
security=security,
|
||||
)
|
||||
),
|
||||
# TODO: Fix this
|
||||
f'{path}/{consts.rest.GUI}': types.rest.api.PathItem(
|
||||
}
|
||||
if cls.REST_API_INFO.gui_type.is_untyped():
|
||||
api_desc[f'{path}/{consts.rest.GUI}'] = types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get GUI representation of {cls_model} items',
|
||||
description=f'Retrieve the GUI representation of {cls_model} items',
|
||||
summary=f'Get GUI representation of {name} items',
|
||||
description=f'Retrieve the GUI representation of {name} items',
|
||||
parameters=[],
|
||||
responses=api_utils.gen_response('GuiElement', with_404=True),
|
||||
responses=api_utils.gen_response('GuiElement', single=False, with_404=True),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
)
|
||||
),
|
||||
f'{path}/{consts.rest.GUI}/{{type}}': types.rest.api.PathItem(
|
||||
)
|
||||
|
||||
if cls.REST_API_INFO.gui_type.is_typed():
|
||||
api_desc[f'{path}/{consts.rest.GUI}/{{type}}'] = types.rest.api.PathItem(
|
||||
get=types.rest.api.Operation(
|
||||
summary=f'Get {cls_model} item by type',
|
||||
description=f'Retrieve a {cls_model} item by type',
|
||||
summary=f'Get GUI representation of {name} type',
|
||||
description=f'Retrieve a {name} GUI representation by type',
|
||||
parameters=[
|
||||
types.rest.api.Parameter(
|
||||
name='type',
|
||||
in_='path',
|
||||
required=True,
|
||||
description='The type of the item',
|
||||
description=f'The type of the {name} GUI representation',
|
||||
schema=types.rest.api.Schema(type='string'),
|
||||
)
|
||||
],
|
||||
responses=api_utils.gen_response('GuiElement', with_404=True),
|
||||
responses=api_utils.gen_response('GuiElement', single=False, with_404=True),
|
||||
tags=get_tags,
|
||||
security=security,
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return api_desc
|
||||
|
@@ -487,6 +487,12 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
with transaction.atomic():
|
||||
userservice = UserService.objects.select_for_update().get(id=userservice.id)
|
||||
operations_logger.info('Removing userservice %a', userservice.name)
|
||||
|
||||
# If already removing or removed, do nothing
|
||||
if State.from_str(userservice.state) in (State.REMOVING, State.REMOVED):
|
||||
logger.debug('Userservice %s already removing or removed', userservice.name)
|
||||
return
|
||||
|
||||
if userservice.is_usable() is False and State.from_str(userservice.state).is_removable() is False:
|
||||
if not forced:
|
||||
raise OperationException(
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import enum
|
||||
import typing
|
||||
import dataclasses
|
||||
|
||||
@@ -25,9 +26,35 @@ def _as_dict_without_none(v: typing.Any) -> typing.Any:
|
||||
return v.as_dict()
|
||||
return v
|
||||
|
||||
|
||||
# This class is used to provide extra information about a handler
|
||||
# (handler, model, detail, etc.)
|
||||
# So we can override names or whatever we need
|
||||
|
||||
# Types of GUI info that can be provided
|
||||
class RestApiInfoGuiType(enum.Enum):
|
||||
UNTYPED = 0
|
||||
TYPED = 1
|
||||
NONE = 3
|
||||
|
||||
def is_untyped(self) -> bool:
|
||||
return self == RestApiInfoGuiType.UNTYPED
|
||||
|
||||
def is_typed(self) -> bool:
|
||||
return self == RestApiInfoGuiType.TYPED
|
||||
|
||||
@dataclasses.dataclass
|
||||
class HandlerApiInfo:
|
||||
pass
|
||||
class RestApiInfo:
|
||||
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
# Guis can have different types:
|
||||
# - MAIN: the gui returns with no type specified (for example, /gui)
|
||||
# - TYPED: the gui returns with a type specified (for example, /gui/whatever_type)
|
||||
# - NONE: no gui is provided
|
||||
gui_type: 'RestApiInfoGuiType' = RestApiInfoGuiType.NONE
|
||||
|
||||
|
||||
# Parameter
|
||||
@dataclasses.dataclass
|
||||
@@ -74,12 +101,14 @@ class Content:
|
||||
class RequestBody:
|
||||
required: bool
|
||||
content: Content # e.g. {'application/json': {'schema': {...}}}
|
||||
description: str | None = None
|
||||
|
||||
def as_dict(self) -> dict[str, typing.Any]:
|
||||
return _as_dict_without_none(
|
||||
{
|
||||
'required': self.required,
|
||||
'content': self.content.as_dict(),
|
||||
'description': self.description,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -159,7 +188,7 @@ class SchemaProperty:
|
||||
enum: list[str | int] | None = None # For enum types
|
||||
properties: dict[str, 'SchemaProperty'] | None = None
|
||||
one_of: list['SchemaProperty'] | None = None
|
||||
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
if not isinstance(value, SchemaProperty):
|
||||
return False
|
||||
@@ -173,7 +202,8 @@ class SchemaProperty:
|
||||
and self.discriminator == value.discriminator
|
||||
and self.enum == value.enum
|
||||
and self.properties == value.properties
|
||||
and sorted(self.one_of or [], key=lambda x: x.type) == sorted(value.one_of or [], key=lambda x: x.type)
|
||||
and sorted(self.one_of or [], key=lambda x: x.type)
|
||||
== sorted(value.one_of or [], key=lambda x: x.type)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -256,9 +286,10 @@ class Schema:
|
||||
properties: dict[str, SchemaProperty] = dataclasses.field(default_factory=dict[str, SchemaProperty])
|
||||
required: list[str] = dataclasses.field(default_factory=list[str])
|
||||
description: str | None = None
|
||||
minimum: int | None = None
|
||||
maximum: int | None = None
|
||||
|
||||
# For use on generating schemas
|
||||
|
||||
def as_dict(self) -> dict[str, typing.Any]:
|
||||
return _as_dict_without_none(
|
||||
{
|
||||
@@ -267,6 +298,8 @@ class Schema:
|
||||
'properties': {k: v.as_dict() for k, v in self.properties.items()} if self.properties else None,
|
||||
'required': self.required if self.required else None,
|
||||
'description': self.description,
|
||||
'minimum': self.minimum,
|
||||
'maximum': self.maximum,
|
||||
}
|
||||
)
|
||||
|
||||
|
@@ -119,9 +119,7 @@ def get_component_from_type(
|
||||
|
||||
components.schemas[type_.type_type] = type_schema
|
||||
|
||||
if is_managed_object and isinstance(
|
||||
item_schema, types.rest.api.Schema
|
||||
):
|
||||
if is_managed_object and isinstance(item_schema, types.rest.api.Schema):
|
||||
# item_schema.discriminator = types.rest.api.Discriminator(propertyName='type')
|
||||
instance_name = f'{item_name}Instance'
|
||||
item_schema.properties['instance'] = types.rest.api.SchemaProperty(
|
||||
@@ -323,7 +321,11 @@ def api_components(
|
||||
|
||||
|
||||
def gen_response(
|
||||
type: str, with_404: bool = False, single: bool = True, delete: bool = False
|
||||
type: str,
|
||||
with_404: bool = False,
|
||||
single: bool = True,
|
||||
delete: bool = False,
|
||||
with_403: bool = True,
|
||||
) -> dict[str, types.rest.api.Response]:
|
||||
data: dict[str, types.rest.api.Response]
|
||||
|
||||
@@ -370,5 +372,92 @@ def gen_response(
|
||||
),
|
||||
),
|
||||
)
|
||||
if with_403:
|
||||
data['403'] = types.rest.api.Response(
|
||||
description='Forbidden. You do not have permission to access this resource with your current role.',
|
||||
content=types.rest.api.Content(
|
||||
media_type='application/json',
|
||||
schema=types.rest.api.SchemaProperty(
|
||||
type='object',
|
||||
properties={
|
||||
'detail': types.rest.api.SchemaProperty(
|
||||
type='string',
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def gen_request_body(type: str, create: bool = True) -> types.rest.api.RequestBody:
|
||||
return types.rest.api.RequestBody(
|
||||
description=f'{"New" if create else "Updated"} {type} item{"s" if not create else ""} to create',
|
||||
required=True,
|
||||
content=types.rest.api.Content(
|
||||
media_type='application/json',
|
||||
schema=types.rest.api.SchemaProperty(
|
||||
type=f'#/components/schemas/{type}' if create else 'array',
|
||||
items=(
|
||||
types.rest.api.SchemaProperty(
|
||||
type=f'#/components/schemas/{type}',
|
||||
)
|
||||
if not create
|
||||
else None
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def gen_odata_parameters() -> list[types.rest.api.Parameter]:
|
||||
return [
|
||||
types.rest.api.Parameter(
|
||||
name='$filter',
|
||||
in_='query',
|
||||
required=False,
|
||||
description='Filter items by property values (e.g., $filter=property eq value)',
|
||||
schema=types.rest.api.Schema(type='string'),
|
||||
),
|
||||
types.rest.api.Parameter(
|
||||
name='$select',
|
||||
in_='query',
|
||||
required=False,
|
||||
description='Select properties to be returned',
|
||||
schema=types.rest.api.Schema(type='string'),
|
||||
),
|
||||
types.rest.api.Parameter(
|
||||
name='$orderby',
|
||||
in_='query',
|
||||
required=False,
|
||||
description='Order items by property values (e.g., $orderby=property desc)',
|
||||
schema=types.rest.api.Schema(type='string'),
|
||||
),
|
||||
types.rest.api.Parameter(
|
||||
name='$top',
|
||||
in_='query',
|
||||
required=False,
|
||||
description='Show only the first N items',
|
||||
schema=types.rest.api.Schema(type='integer', format='int32', minimum=1),
|
||||
),
|
||||
types.rest.api.Parameter(
|
||||
name='$skip',
|
||||
in_='query',
|
||||
required=False,
|
||||
description='Skip the first N items',
|
||||
schema=types.rest.api.Schema(type='integer', format='int32', minimum=0),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def gen_uuid_parameters(with_odata: bool) -> list[types.rest.api.Parameter]:
|
||||
return [
|
||||
types.rest.api.Parameter(
|
||||
name='uuid',
|
||||
in_='path',
|
||||
required=True,
|
||||
description='The UUID of the item',
|
||||
schema=types.rest.api.Schema(type='string', format='uuid'),
|
||||
)
|
||||
] + (gen_odata_parameters() if with_odata else [])
|
||||
|
@@ -67,7 +67,7 @@ class BaseRestItem(types.rest.BaseRestItem):
|
||||
|
||||
class TestTransport(transports.Transport):
|
||||
"""
|
||||
Simpe testing transport. Currently a copy of URLCustomTransport
|
||||
Simpe testing transport.
|
||||
"""
|
||||
|
||||
type_name = 'Test Transport'
|
||||
|
Reference in New Issue
Block a user