Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions api/server/routes/aas.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
from typing import Any

from aas_core3.types import Identifiable
from fastapi import APIRouter, Request
from fastapi import APIRouter, Request, HTTPException

from server.services.aas_service import AasService
from server.utils.pagination import Pagination
from basyx import ObjectStore

from server.utils.decorator import paginated

class AasRouter(Pagination):
class AasRouter:
def __init__(self, global_obj_store: ObjectStore[Identifiable]):
self.router = APIRouter()
self.service = AasService(global_obj_store)
self._setup_routes()

def _setup_routes(self):
@self.router.get("/shells")
async def get_all_aas() -> Any:
@paginated()
async def get_all_aas(request: Request) -> Any:
return self.service.get_all_shells_as_jsonable()

@self.router.post("/shells")
Expand All @@ -26,7 +27,7 @@ async def create_aas(request: Request) -> Any:

@self.router.get("/shells/$reference")
async def get_all_aas_reference() -> Any:
return {"message": "Content parameters are not supported yet."}
raise HTTPException(status_code=501, detail="This route is not yet implemented!")

@self.router.get("/shells/{aas_identifier}")
async def get_aas_by_id(aas_identifier: str) -> Any:
Expand Down
5 changes: 4 additions & 1 deletion api/server/routes/aas_registry_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from server.services.aas_registry_server_service import AasRegistryServerService
from basyx import ObjectStore

from server.utils.decorator import paginated


class AasRegistryRouter:
def __init__(self, global_obj_store: ObjectStore[Identifiable]):
Expand All @@ -16,7 +18,8 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]):

def _setup_routes(self):
@self.router.get("/")
async def get_all_aas_descriptors() -> Any:
@paginated()
async def get_all_aas_descriptors(request: Request) -> Any:
return self.service.get_all_asset_administration_shell_descriptors()

@self.router.get("/{aas_descriptor_id}")
Expand Down
5 changes: 4 additions & 1 deletion api/server/routes/aasx_file_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from server.services.aasx_file_server_service import AasxFileServerService
from basyx import ObjectStore

from server.utils.decorator import paginated


class AasxFileServerRouter:
def __init__(self, global_obj_store: ObjectStore[Identifiable]):
Expand All @@ -16,7 +18,8 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]):

def _setup_routes(self):
@self.router.get("")
async def get_all_aasx() -> Any:
@paginated()
async def get_all_aasx(request: Request) -> Any:
return self.service.get_all_aasx_package_ids()

@self.router.get("/{aasx_package_id}")
Expand Down
19 changes: 10 additions & 9 deletions api/server/routes/submodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
from server.services.submodel_service import SubmodelService
from basyx import ObjectStore

from server.utils.pagination import Pagination
from server.utils.decorator import limited
from server.utils.decorator import paginated


class SubmodelRouter(Pagination):
class SubmodelRouter:
def __init__(self, global_obj_store: ObjectStore[Identifiable]):
self.router = APIRouter()
self.obj_store = global_obj_store
Expand All @@ -20,24 +19,24 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]):
def _setup_routes(self):
# GetAllSubmodels and path-suffixes
@self.router.get("")
@limited()
@paginated()
async def get_submodel_all(request: Request) -> Any:
return self.service.get_all_submodels_as_jsonables()

@self.router.get("/$metadata")
@limited()
@paginated()
async def get_submodel_all_metadata(request: Request) -> Any:
# Returns metadata for all submodels, stripped of detailed content
raise HTTPException(status_code=501, detail="This route is not yet implemented!")

@self.router.get("/$reference")
@limited()
@paginated()
async def get_submodel_all_reference(request: Request) -> Any:
# Returns references for all submodels without full data
raise HTTPException(status_code=501, detail="This route is not yet implemented!")

@self.router.get("/$value")
@limited()
@paginated()
async def not_implemented_value(request: Request) -> Any:
raise HTTPException(status_code=501, detail="This route is not implemented!")

Expand Down Expand Up @@ -99,7 +98,8 @@ async def post_submodel_elements(submodel_identifier: str, request: Request) ->
return self.service.post_submodel_element(submodel_identifier, body)

@self.router.get("/{submodel_identifier}/submodel-elements")
async def get_submodel_submodel_elements(submodel_identifier: str) -> Any:
@paginated()
async def get_submodel_submodel_elements(request: Request, submodel_identifier: str) -> Any:
# Get submodel elements
self.service.get_submodel_elements(submodel_identifier)

Expand All @@ -121,7 +121,8 @@ async def not_implemented_submodel_elements_path(submodel_identifier: str) -> An
raise HTTPException(status_code=501, detail="This route is not yet implemented!")

@self.router.get("/{submodel_identifier}/submodel-elements/{id_short_path}")
async def get_submodel_submodel_elements_id_short_path(submodel_identifier: str, id_short_path: str) -> Any:
@paginated()
async def get_submodel_submodel_elements_id_short_path(request: Request, submodel_identifier: str, id_short_path: str) -> Any:
return self.service.get_submodel_element(submodel_identifier, id_short_path)

@self.router.post("/{submodel_identifier}/submodel-elements/{id_short_path}")
Expand Down
5 changes: 4 additions & 1 deletion api/server/routes/submodel_registry_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from server.services.submodel_registry_server_service import SubmodelRegistryServerService
from basyx import ObjectStore

from server.utils.decorator import paginated


class SubmodelRegistryRouter:
def __init__(self, global_obj_store: ObjectStore[Identifiable]):
Expand All @@ -16,7 +18,8 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]):

def _setup_routes(self):
@self.router.get("/")
async def get_all_submodel_descriptors() -> Any:
@paginated()
async def get_all_submodel_descriptors(request: Request) -> Any:
return self.service.get_all_submodel_descriptors()

@self.router.get("/{submodel_id}")
Expand Down
28 changes: 18 additions & 10 deletions api/server/utils/decorator.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
from functools import wraps
from fastapi import Request
from typing import Callable, Any, Union
from typing import Callable, Any, Union, Tuple


def limited(default_limit: int = 100):
def paginated(default_limit: int = 100):
def decorator(func: Callable):
@wraps(func)
async def wrapper(*args, **kwargs) -> Any:
# Extract request from kwargs (FastAPI automatically passes it)
request: Request = kwargs.get("request")
limit: Union[str, int] = request.query_params.get("limit", default_limit)
query = request.query_params

try:
limit = int(limit) # Ensure limit is an integer
limit = int(query.get("limit", default_limit))
cursor = int(query.get("cursor", 0))
if limit < 0 or cursor < 0:
raise ValueError
except ValueError:
limit = default_limit # Fallback if conversion fails
from fastapi import HTTPException
raise HTTPException(status_code=400, detail="Cursor and limit must be >0")

# Call the original function
result = await func(*args, **kwargs)

# Apply limit if the result is a list
if isinstance(result, list):
return result[:limit]
return result # If not a list, return as-is
# FIXME: Is this correct? Following Spec p.119
return {
"result": result[cursor:cursor + limit],
"paging_metadata": {
"next_cursor": cursor + limit if cursor + limit < len(result) else None
}
}

return result

return wrapper

Expand Down
7 changes: 0 additions & 7 deletions api/server/utils/pagination.py

This file was deleted.

7 changes: 7 additions & 0 deletions api/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def wrap_paginated(result):
return {
"result": result,
"paging_metadata": {
"next_cursor": None
}
}
6 changes: 6 additions & 0 deletions api/test/examples/empty_paged_result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"result": [],
"paging_metadata": {
"next_cursor": null
}
}
37 changes: 37 additions & 0 deletions api/test/examples/submodel/submodel2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"id": "urn:x-test:submodel2",
"submodelElements": [
{
"idShort": "some_property",
"valueType": "xs:int",
"value": "1984",
"modelType": "Property"
},
{
"idShort": "some_blob",
"value": "3q2+7w==",
"contentType": "application/octet-stream",
"modelType": "Blob"
},
{
"idShort": "ExampleSubmodelList",
"typeValueListElement": "SubmodelElementList",
"value": [
{
"idShort": "list_1",
"value": "3q2+7w==",
"contentType": "application/octet-stream",
"modelType": "Blob"
},
{
"idShort": "list_2",
"value": "3q2+7w==",
"contentType": "application/octet-stream",
"modelType": "Blob"
}
],
"modelType": "SubmodelElementList"
}
],
"modelType": "Submodel"
}
9 changes: 7 additions & 2 deletions api/test/test_aas_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from server import app

from test import wrap_paginated

BASE_URL = "/api/v3.0/"


Expand All @@ -31,6 +33,9 @@ def setUp(self):
with open(os.path.join(base_path, "examples/aas", "thumbnail_modified.json"), encoding="utf-8") as f:
self.thumbnail_example_modified = json.load(f)

with open(os.path.join(base_path, "examples", "empty_paged_result.json"), encoding="utf-8") as f:
self.empty_result = json.load(f)

# FIXME: modified AAS should contain more complex types but deserialization seems to fail
with open(os.path.join(base_path, "examples/aas", "aas_modified.json"), encoding="utf-8") as f:
self.aas_example_modified = json.load(f)
Expand All @@ -48,14 +53,14 @@ def test_aas_post(self):
def test_get_all_shells(self):
response = self.client.get(BASE_URL + "aas/shells")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
self.assertEqual(response.json(), self.empty_result)

# Setup
self.client.post(BASE_URL + "aas/shells", json=self.aas_example)

response = self.client.get(BASE_URL + "aas/shells")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [self.aas_example])
self.assertEqual(response.json(), wrap_paginated([self.aas_example]))

# Teardown
self.client.delete(BASE_URL + "ass/shells/" + self.shell_example_id)
Expand Down
10 changes: 8 additions & 2 deletions api/test/test_aasx_file_server_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from server import app
from aas_core3 import jsonization

from test import wrap_paginated

client = TestClient(app)
BASE_URL = "/api/v3.0/"

Expand All @@ -18,21 +20,25 @@ def setUp(self):
with open(os.path.join(base_path, "examples/aasx", "aasx.json"), encoding="utf-8") as f:
self.aasx_json = json.load(f)

with open(os.path.join(base_path, "examples", "empty_paged_result.json"), encoding="utf-8") as f:
self.empty_result = json.load(f)


self.test_aasx_id = self.aasx_json["id"]
self.aasx = jsonization.asset_administration_shell_from_jsonable(self.aasx_json)

def test_get_all_aasx_package_ids(self):
# Test empty
response = self.client.get(BASE_URL + "aasx")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
self.assertEqual(response.json(), self.empty_result)

# Setup
self.client.post(BASE_URL + "aasx", json=self.aasx_json)

response = self.client.get(BASE_URL + "aasx")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [self.test_aasx_id])
self.assertEqual(response.json(), wrap_paginated([self.test_aasx_id]))

# Teardown
self.client.delete(BASE_URL + "aasx/" + self.test_aasx_id)
Expand Down
Loading