1
0
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:
Adolfo Gómez García
2025-09-20 16:17:18 +02:00
parent 4cedf057b3
commit 00fb79244a
25 changed files with 317 additions and 134 deletions

View File

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

View File

@@ -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(

View File

@@ -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()

View File

@@ -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(

View File

@@ -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(

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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(

View File

@@ -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()

View File

@@ -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':

View File

@@ -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]:

View File

@@ -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()

View File

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

View File

@@ -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]:

View File

@@ -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()

View File

@@ -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()

View File

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

View File

@@ -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)

View File

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

View File

@@ -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(

View File

@@ -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,
}
)

View File

@@ -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 [])

View File

@@ -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'