laia-gen-lib 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. laia-gen-lib-0.1.0/PKG-INFO +5 -0
  2. laia-gen-lib-0.1.0/laia_gen_lib.egg-info/PKG-INFO +5 -0
  3. laia-gen-lib-0.1.0/laia_gen_lib.egg-info/SOURCES.txt +27 -0
  4. laia-gen-lib-0.1.0/laia_gen_lib.egg-info/dependency_links.txt +1 -0
  5. laia-gen-lib-0.1.0/laia_gen_lib.egg-info/requires.txt +3 -0
  6. laia-gen-lib-0.1.0/laia_gen_lib.egg-info/top_level.txt +2 -0
  7. laia-gen-lib-0.1.0/laiagenlib/__init__.py +3 -0
  8. laia-gen-lib-0.1.0/laiagenlib/crud/__init__.py +2 -0
  9. laia-gen-lib-0.1.0/laiagenlib/crud/crud.py +24 -0
  10. laia-gen-lib-0.1.0/laiagenlib/crud/crud_mongo_impl.py +69 -0
  11. laia-gen-lib-0.1.0/laiagenlib/crud/schemas.py +13 -0
  12. laia-gen-lib-0.1.0/laiagenlib/main.py +33 -0
  13. laia-gen-lib-0.1.0/laiagenlib/models/AccessRights.py +73 -0
  14. laia-gen-lib-0.1.0/laiagenlib/models/Model.py +165 -0
  15. laia-gen-lib-0.1.0/laiagenlib/models/Openapi.py +128 -0
  16. laia-gen-lib-0.1.0/laiagenlib/models/Role.py +27 -0
  17. laia-gen-lib-0.1.0/laiagenlib/models/Route.py +10 -0
  18. laia-gen-lib-0.1.0/laiagenlib/models/__init__.py +5 -0
  19. laia-gen-lib-0.1.0/laiagenlib/utils/__init__.py +2 -0
  20. laia-gen-lib-0.1.0/laiagenlib/utils/logger.py +34 -0
  21. laia-gen-lib-0.1.0/laiagenlib/utils/utils.py +41 -0
  22. laia-gen-lib-0.1.0/setup.cfg +4 -0
  23. laia-gen-lib-0.1.0/setup.py +13 -0
  24. laia-gen-lib-0.1.0/tests/__init__.py +0 -0
  25. laia-gen-lib-0.1.0/tests/models/__init__.py +0 -0
  26. laia-gen-lib-0.1.0/tests/models/test_access_rights.py +155 -0
  27. laia-gen-lib-0.1.0/tests/models/test_model.py +258 -0
  28. laia-gen-lib-0.1.0/tests/models/test_openapi.py +26 -0
  29. laia-gen-lib-0.1.0/tests/models/test_role.py +58 -0
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.1
2
+ Name: laia-gen-lib
3
+ Version: 0.1.0
4
+ Summary: An AI application generator engine
5
+ Author: Me
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.1
2
+ Name: laia-gen-lib
3
+ Version: 0.1.0
4
+ Summary: An AI application generator engine
5
+ Author: Me
@@ -0,0 +1,27 @@
1
+ setup.py
2
+ laia_gen_lib.egg-info/PKG-INFO
3
+ laia_gen_lib.egg-info/SOURCES.txt
4
+ laia_gen_lib.egg-info/dependency_links.txt
5
+ laia_gen_lib.egg-info/requires.txt
6
+ laia_gen_lib.egg-info/top_level.txt
7
+ laiagenlib/__init__.py
8
+ laiagenlib/main.py
9
+ laiagenlib/crud/__init__.py
10
+ laiagenlib/crud/crud.py
11
+ laiagenlib/crud/crud_mongo_impl.py
12
+ laiagenlib/crud/schemas.py
13
+ laiagenlib/models/AccessRights.py
14
+ laiagenlib/models/Model.py
15
+ laiagenlib/models/Openapi.py
16
+ laiagenlib/models/Role.py
17
+ laiagenlib/models/Route.py
18
+ laiagenlib/models/__init__.py
19
+ laiagenlib/utils/__init__.py
20
+ laiagenlib/utils/logger.py
21
+ laiagenlib/utils/utils.py
22
+ tests/__init__.py
23
+ tests/models/__init__.py
24
+ tests/models/test_access_rights.py
25
+ tests/models/test_model.py
26
+ tests/models/test_openapi.py
27
+ tests/models/test_role.py
@@ -0,0 +1,3 @@
1
+ pymongo
2
+ pydantic
3
+ datamodel-code-generator
@@ -0,0 +1,2 @@
1
+ laiagenlib
2
+ tests
@@ -0,0 +1,3 @@
1
+ from . import crud
2
+ from . import models
3
+ from . import utils
@@ -0,0 +1,2 @@
1
+ from . import crud
2
+ from . import crud_mongo_impl
@@ -0,0 +1,24 @@
1
+ from typing import TypeVar, Optional, Dict
2
+ from pydantic import BaseModel
3
+
4
+ T = TypeVar('T', bound='BaseModel')
5
+
6
+ class CRUD:
7
+
8
+ def __init__(self, db: Dict[str, any]):
9
+ self.db = db
10
+
11
+ async def get_items(model_name: str, skip: int = 0, limit: int = 10, filters: Optional[dict] = None):
12
+ pass
13
+
14
+ async def get_item(model_name: str, item_id: str):
15
+ pass
16
+
17
+ async def post_item(model_name: str, item: T):
18
+ pass
19
+
20
+ async def put_item(model_name: str, item_id: str, update_fields: dict):
21
+ pass
22
+
23
+ async def delete_item(model_name: str, item_id: str):
24
+ pass
@@ -0,0 +1,69 @@
1
+ from typing import TypeVar, Optional, Dict
2
+ from pydantic import BaseModel
3
+ from pymongo.collection import ReturnDocument
4
+ from .schemas import list_serial, individual_serial
5
+ from bson import ObjectId
6
+ from .crud import CRUD
7
+
8
+ T = TypeVar('T', bound='BaseModel')
9
+
10
+ class CRUDMongoImpl(CRUD):
11
+
12
+ def __init__(self, db: Dict[str, any]):
13
+ super().__init__(db)
14
+
15
+ async def get_items(self, model_name: str, skip: int = 0, limit: int = 10, filters: Optional[dict] = None, orders: Optional[dict] = None):
16
+ collection = self.db[model_name]
17
+
18
+ query = filters or {}
19
+ sorts = orders or {}
20
+
21
+
22
+ items = collection.find(query, skip=skip, limit=limit, sort=sorts)
23
+ serialized_items = list_serial(items)
24
+
25
+ total_count = collection.count_documents(query)
26
+
27
+ return serialized_items, total_count
28
+
29
+ async def get_item(self, model_name: str, item_id: str):
30
+ collection = self.db[model_name]
31
+
32
+ item = collection.find_one({'_id': ObjectId(item_id)})
33
+
34
+ if item:
35
+ return individual_serial(item)
36
+ raise ValueError(f"{model_name} with ID {item_id} not found")
37
+
38
+ async def post_item(self, model_name: str, item: T):
39
+ collection = self.db[model_name]
40
+ item_dict = dict(item)
41
+ item_dict.pop('id', None)
42
+ created_result = collection.insert_one(item_dict)
43
+ inserted_id = created_result.inserted_id
44
+ item_dict['id'] = str(inserted_id)
45
+ item_dict.pop('_id', None)
46
+
47
+ return item_dict
48
+
49
+ async def put_item(self, model_name: str, item_id: str, update_fields: dict):
50
+ collection = self.db[model_name]
51
+ update_query = {'$set': update_fields}
52
+
53
+ updated_item = collection.find_one_and_update(
54
+ {'_id': ObjectId(item_id)},
55
+ update_query,
56
+ return_document=ReturnDocument.AFTER,
57
+ )
58
+
59
+ if updated_item:
60
+ return individual_serial(updated_item)
61
+ raise Exception
62
+
63
+ async def delete_item(self, model_name: str, item_id: str):
64
+ collection = self.db[model_name]
65
+ deleted_item = collection.find_one_and_delete({'_id': ObjectId(item_id)})
66
+ if deleted_item:
67
+ return individual_serial(deleted_item)
68
+ raise Exception
69
+
@@ -0,0 +1,13 @@
1
+ from typing import List, TypeVar, Type
2
+ import json
3
+
4
+ T = TypeVar('T', bound='BaseModel')
5
+
6
+ def individual_serial(item: T) -> dict:
7
+ item['_id'] = str(item['_id'])
8
+ item['id'] = str(item['_id'])
9
+ del item['_id']
10
+ return item
11
+
12
+ def list_serial(items: List[T]) -> list[dict]:
13
+ return[individual_serial(item) for item in items]
@@ -0,0 +1,33 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from .crud.crud import CRUD
4
+ from .models.Openapi import OpenAPI
5
+ from .utils.utils import create_models_file
6
+ from .utils.logger import _logger
7
+ import os
8
+
9
+ class LaiaFastApi():
10
+
11
+ def __init__(self, openapi, db, crud: CRUD):
12
+ self.db = db
13
+ self.crud_instance = crud(db)
14
+ self.openapi_path = openapi
15
+ self.openapi = OpenAPI(openapi)
16
+ self.api = FastAPI()
17
+ self.api.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=["*"],
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ models_dir = os.path.join(os.path.dirname(self.openapi_path), "models")
26
+ if not os.path.exists(models_dir):
27
+ os.makedirs(models_dir)
28
+
29
+ models_path = os.path.join(models_dir, "models.py")
30
+ create_models_file(self.openapi_path, models_path)
31
+ self.openapi.create_crud_routes(self.api, self.crud_instance, models_path)
32
+
33
+
@@ -0,0 +1,73 @@
1
+ from typing import Dict, Type
2
+ from pydantic import BaseModel
3
+ from ..crud.crud import CRUD
4
+ from ..utils.logger import _logger
5
+ from ..utils.utils import create_element
6
+
7
+ class AccessRights(BaseModel):
8
+ role: str
9
+ model: str
10
+ operations: Dict[str, int] = {}
11
+ fields_create: Dict[str, int] = {}
12
+ fields_edit: Dict[str, int] = {}
13
+ fields_visible: Dict[str, int] = {}
14
+
15
+ @classmethod
16
+ async def create(cls, new_access_rights: dict, model: Type, user_roles: list, crud_instance: CRUD):
17
+ _logger.info(f"Creating new AccessRights with values: {new_access_rights}")
18
+
19
+ if 'role' in new_access_rights and 'model' in new_access_rights:
20
+ pass
21
+ else:
22
+ raise ValueError("Missing required parameters")
23
+
24
+ if new_access_rights.get("model") != model.__name__.lower():
25
+ raise ValueError("Provided model name does not match the class model name")
26
+
27
+ if "admin" not in user_roles:
28
+ raise PermissionError("Only users with 'admin' role can create access rights")
29
+
30
+ operations = new_access_rights.get("operations", {})
31
+ valid_operations = {"create", "read", "update", "delete", "search"}
32
+
33
+ for operation in operations:
34
+ if operation not in valid_operations or not isinstance(operations[operation], int):
35
+ raise ValueError(f"Invalid format for operation {operation}")
36
+
37
+ fields_to_check = ["fields_create", "fields_edit", "fields_visible"]
38
+
39
+ for field_type in fields_to_check:
40
+ fields = new_access_rights.get(field_type, {})
41
+ if not isinstance(fields, dict):
42
+ raise ValueError(f"Invalid format for {field_type}")
43
+
44
+ model_fields = []
45
+ for class_in_hierarchy in model.mro():
46
+ if hasattr(class_in_hierarchy, '__annotations__'):
47
+ model_fields.extend([field for field in class_in_hierarchy.__annotations__ if not field.startswith("_")])
48
+
49
+ for field_name, field_value in fields.items():
50
+ if field_name not in model_fields or not isinstance(field_value, int):
51
+ raise ValueError(f"Invalid field {field_name} for {field_type}")
52
+
53
+ existing_access_rights, _ = await crud_instance.get_items(
54
+ "accessrights",
55
+ skip=0,
56
+ limit=10,
57
+ filters={
58
+ "model": new_access_rights.get("model"),
59
+ "role": new_access_rights.get("role")
60
+ }
61
+ )
62
+
63
+ if existing_access_rights:
64
+ raise ValueError("AccessRights with the same role and model already exists")
65
+
66
+ try:
67
+ access_rights = AccessRights(**new_access_rights)
68
+ except Exception:
69
+ raise ValueError("Missing required parameters")
70
+
71
+ created_accessrights = await create_element(access_rights, crud_instance)
72
+ _logger.info("AccessRights created successfully")
73
+ return cls(**created_accessrights)
@@ -0,0 +1,165 @@
1
+ from typing import Type, List, Dict, Any
2
+ from pydantic import BaseModel
3
+ from ..utils.utils import create_element
4
+ from ..crud.crud import CRUD
5
+ from .AccessRights import AccessRights
6
+ from ..utils.logger import _logger
7
+
8
+ class LaiaBaseModel(BaseModel):
9
+ id: str = ""
10
+ name: str
11
+
12
+ @classmethod
13
+ async def create(cls, new_element: dict, model: Type, user_roles: list, crud_instance: CRUD):
14
+ _logger.info(f"Creating new {model.__name__} with values: {new_element}")
15
+
16
+ try:
17
+ element = model(**new_element)
18
+ except Exception:
19
+ raise ValueError("Missing required parameters")
20
+
21
+ if "admin" not in user_roles:
22
+ model_name = model.__name__.lower()
23
+ access_rights_list = await cls.check_access_rights(model_name, user_roles, "create", crud_instance)
24
+ await cls.check_fields_permission(model, 'fields_create', new_element, access_rights_list)
25
+
26
+ created_element = await create_element(element, crud_instance)
27
+
28
+ if "admin" not in user_roles:
29
+ allowed_fields = cls.get_allowed_fields(access_rights_list, 'fields_visible')
30
+ created_element = {field: created_element[field] for field in allowed_fields if field in created_element}
31
+
32
+ _logger.info(f"{model.__name__} created successfully")
33
+ return created_element
34
+
35
+ @classmethod
36
+ async def update(cls, element_id:str, updated_values: dict, model: Type, user_roles: list, crud_instance: CRUD):
37
+ _logger.info(f"Updating {model.__name__} with ID: {element_id} and values: {updated_values}")
38
+
39
+ model_name = model.__name__.lower()
40
+
41
+ if "admin" not in user_roles:
42
+ access_rights_list = await cls.check_access_rights(model_name, user_roles, "update", crud_instance)
43
+ await cls.check_fields_permission(model, 'fields_edit', updated_values, access_rights_list)
44
+
45
+ try:
46
+ updated_element = await crud_instance.put_item(model_name, element_id, updated_values)
47
+ except Exception:
48
+ raise ValueError(f"{model.__name__} with ID does not exist, or the updating parameters have errors")
49
+
50
+ if "admin" not in user_roles:
51
+ allowed_fields = cls.get_allowed_fields(access_rights_list, 'fields_visible')
52
+ updated_element = {field: updated_element[field] for field in allowed_fields if field in updated_element}
53
+
54
+ _logger.info(f"{model.__name__} created successfully")
55
+ return updated_element
56
+
57
+ @classmethod
58
+ async def delete(cls, element_id: str, model: Type, user_roles: List[str], crud_instance: CRUD):
59
+ _logger.info(f"Deleting {model.__name__} with ID: {element_id}")
60
+
61
+ model_name = model.__name__.lower()
62
+
63
+ if "admin" not in user_roles:
64
+ await cls.check_access_rights(model_name, user_roles, "delete", crud_instance)
65
+ try:
66
+ await crud_instance.delete_item(model_name, element_id)
67
+ except Exception:
68
+ raise ValueError(f"{model.__name__} with ID does not exist, or there was an error deleting the element")
69
+
70
+ _logger.info(f"{model.__name__} deleted successfully")
71
+
72
+ @classmethod
73
+ async def read(cls, element_id: str, model: Type, user_roles: List[str], crud_instance: CRUD):
74
+ _logger.info(f"Getting {model.__name__} with ID: {element_id}")
75
+
76
+ model_name = model.__name__.lower()
77
+
78
+ if "admin" not in user_roles:
79
+ access_rights_list = await cls.check_access_rights(model_name, user_roles, "read", crud_instance)
80
+ try:
81
+ item = await crud_instance.get_item(model_name, element_id)
82
+ except ValueError as e:
83
+ raise ValueError(str(e))
84
+
85
+ if "admin" not in user_roles:
86
+ allowed_fields = cls.get_allowed_fields(access_rights_list, 'fields_visible')
87
+ item = {field: item[field] for field in allowed_fields if field in item}
88
+
89
+ _logger.info(f"{model.__name__} retrieved successfully")
90
+ return item
91
+
92
+ @classmethod
93
+ async def search(cls, filters: dict, model: Type, user_roles: List[str], crud_instance: CRUD):
94
+ _logger.info(f"Searching {model.__name__} with filters: {filters}")
95
+
96
+ model_name = model.__name__.lower()
97
+
98
+ if "admin" not in user_roles:
99
+ await cls.check_access_rights(model_name, user_roles, "search", crud_instance)
100
+
101
+ try:
102
+ items, total_count = await crud_instance.get_items(model_name, filters=filters)
103
+ except Exception:
104
+ raise ValueError(f"Error occurred while searching {model.__name__} with filters: {filters}")
105
+
106
+ _logger.info(f"{model.__name__} search completed successfully")
107
+ return items, total_count
108
+
109
+ @classmethod
110
+ async def check_access_rights(cls, model_name: str, roles: List[str], operation: str, crud_instance: CRUD):
111
+ access_rights_list = []
112
+
113
+ for role in roles:
114
+ access_rights, _ = await crud_instance.get_items(
115
+ "accessrights",
116
+ skip=0,
117
+ limit=1,
118
+ filters={
119
+ "model": model_name,
120
+ "role": role
121
+ }
122
+ )
123
+
124
+ if access_rights and access_rights[0]["operations"].get(operation, 0) >= 1:
125
+ access_rights_list.append(access_rights[0])
126
+
127
+ if not access_rights_list:
128
+ raise PermissionError(f"None of the roles have sufficient permissions for operation '{operation}' on model '{model_name}'")
129
+
130
+ return access_rights_list
131
+
132
+ @classmethod
133
+ async def check_fields_permission(cls, model, fields_type: str, new_element: Dict[str, int], access_rights_list: List[AccessRights]):
134
+ model_fields = []
135
+
136
+ for class_in_hierarchy in model.mro():
137
+ if hasattr(class_in_hierarchy, '__annotations__'):
138
+ model_fields.extend([field for field in class_in_hierarchy.__annotations__ if not field.startswith("_")])
139
+
140
+ for field_name, field_value in new_element.items():
141
+ if field_name not in model_fields:
142
+ raise ValueError(f"Invalid field {field_name}")
143
+
144
+ for field_name, field_value in new_element.items():
145
+ allowed_by_some_role = False
146
+
147
+ for access_rights in access_rights_list:
148
+ if access_rights.get(fields_type).get(field_name, {}) == 1:
149
+ allowed_by_some_role = True
150
+ break
151
+
152
+ if not allowed_by_some_role:
153
+ raise PermissionError(f"Insufficient permissions to create the field '{field_name}' in any role.")
154
+
155
+ @classmethod
156
+ def get_allowed_fields(cls, access_rights_list: List[AccessRights], fields_type: str):
157
+ allowed_fields = set()
158
+
159
+ for access_rights in access_rights_list:
160
+ fields = access_rights.get(fields_type, {})
161
+ for field_name, field_value in fields.items():
162
+ if field_value == 1:
163
+ allowed_fields.add(field_name)
164
+
165
+ return allowed_fields
@@ -0,0 +1,128 @@
1
+ import yaml
2
+ from fastapi import FastAPI, HTTPException, status
3
+ from fastapi.routing import APIRouter
4
+ from typing import TypeVar, Optional
5
+ import re
6
+ from importlib.util import spec_from_file_location, module_from_spec
7
+ from .Route import Route
8
+ from ..crud.crud import CRUD
9
+ from .Model import LaiaBaseModel
10
+ from ..utils.logger import _logger
11
+
12
+ T = TypeVar('T', bound='LaiaBaseModel')
13
+
14
+ class OpenAPI:
15
+ def __init__(self, yaml_path):
16
+ self.yaml_path = yaml_path
17
+ self.routes = []
18
+ self.models = []
19
+
20
+ self.parse_yaml()
21
+
22
+ def parse_yaml(self):
23
+ with open(self.yaml_path, 'r') as file:
24
+ openapi_spec = yaml.safe_load(file)
25
+
26
+ if 'paths' in openapi_spec:
27
+ for path, path_data in openapi_spec['paths'].items():
28
+ methods = path_data.keys()
29
+ for method in methods:
30
+ if method != "parameters":
31
+ summary = path_data[method].get('summary', '')
32
+ responses = path_data[method].get('responses', {})
33
+ extensions = path_data[method].get('x-', {})
34
+ self.routes.append(Route(path, method, summary, responses, extensions))
35
+
36
+ if 'components' in openapi_spec:
37
+ schemas = openapi_spec['components'].get('schemas', {})
38
+ self.models = list(schemas.keys())
39
+
40
+ def create_crud_routes(self, api: FastAPI=None, crud_instance: CRUD=None, models_path: str=""):
41
+ for model_name in self.models:
42
+ model_module = self.import_model(models_path)
43
+ model = getattr(model_module, model_name)
44
+ model_lowercase = model_name.lower()
45
+ model_plural = model_lowercase + 's' # TODO: use it for search path
46
+
47
+ create_route = None
48
+ read_route = None
49
+ update_route = None
50
+ delete_route = None
51
+ search_route = None
52
+
53
+ for route in self.routes:
54
+ if route.extensions.get(f'x-create-{model_lowercase}'):
55
+ create_route = route.path
56
+ elif route.extensions.get(f'x-read-{model_lowercase}'):
57
+ read_route = route.path
58
+ elif route.extensions.get(f'x-update-{model_lowercase}'):
59
+ update_route = route.path
60
+ elif route.extensions.get(f'x-delete-{model_lowercase}'):
61
+ delete_route = route.path
62
+ elif route.extensions.get(f'x-search-{model_lowercase}'):
63
+ search_route = route.path
64
+
65
+ self.CRUD(
66
+ api=api,
67
+ model=model,
68
+ create_route=create_route,
69
+ read_route=read_route,
70
+ update_route=update_route,
71
+ delete_route=delete_route,
72
+ search_route=search_route
73
+ )
74
+
75
+ def CRUD(self, api: FastAPI, model: T, create_route: Optional[str] = None, read_route: Optional[str] = None, update_route: Optional[str] = None, delete_route: Optional[str] = None, search_route: Optional[str] = None):
76
+ model_name = model.__name__.lower()
77
+ router = APIRouter(tags=[model.__name__])
78
+
79
+ def replace_placeholder(route: str) -> str:
80
+ return re.sub(r'\{.*?\}', '{element_id}', route)
81
+
82
+ create_route = create_route or f"/{model_name}/"
83
+ read_route = read_route or f"/{model_name}"+"/{element_id}"
84
+ update_route = update_route or f"/{model_name}"+"/{element_id}"
85
+ delete_route = delete_route or f"/{model_name}"+"/{element_id}"
86
+ search_route = search_route or None # TODO: change to the defined route
87
+
88
+
89
+ @router.post(create_route, response_model=dict)
90
+ async def create_element(element: model):
91
+ user_roles=["admin"]
92
+ try:
93
+ return await model.create(dict(element), model, user_roles, self.crud_instance)
94
+ except Exception as e:
95
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
96
+
97
+ @router.put(update_route, response_model=dict)
98
+ async def update_element(element_id: str, values: dict):
99
+ user_roles=["admin"]
100
+ try:
101
+ return await model.update(element_id, values, model, user_roles, self.crud_instance)
102
+ except Exception as e:
103
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
104
+
105
+ @router.get(read_route, response_model=dict)
106
+ async def read_element(element_id: str):
107
+ user_roles=["admin"]
108
+ try:
109
+ return await model.read(element_id, model, user_roles, self.crud_instance)
110
+ except Exception as e:
111
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
112
+
113
+ @router.delete(delete_route, response_model=str)
114
+ async def delete_element(element_id: str):
115
+ user_roles=["admin"]
116
+ try:
117
+ await model.delete(element_id, model, user_roles, self.crud_instance)
118
+ return f"{model_name} element deleted successfully"
119
+ except Exception as e:
120
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
121
+
122
+ api.include_router(router)
123
+
124
+ def import_model(self, models_path):
125
+ spec = spec_from_file_location("models", models_path)
126
+ module = module_from_spec(spec)
127
+ spec.loader.exec_module(module)
128
+ return module
@@ -0,0 +1,27 @@
1
+ from typing import Dict
2
+ from pydantic import BaseModel
3
+ from ..crud.crud import CRUD
4
+ from ..utils.logger import _logger
5
+ from ..utils.utils import create_element
6
+
7
+ class Role(BaseModel):
8
+ name: str
9
+
10
+ @classmethod
11
+ async def create(cls, new_role: dict, user_roles: list, crud_instance: CRUD):
12
+ _logger.info(f"Creating new Role with values: {new_role}")
13
+
14
+ if "name" not in new_role:
15
+ raise ValueError("Missing required parameter: name")
16
+
17
+ role = Role(**new_role)
18
+ if "admin" not in user_roles:
19
+ raise PermissionError("Only users with 'admin' role can create new roles")
20
+
21
+ existing_roles, _ = await crud_instance.get_items("role", skip=0, limit=10, filters={"name": role.name})
22
+ if existing_roles:
23
+ raise ValueError(f"Role with name '{role.name}' already exists")
24
+
25
+ created_role = await create_element(role, crud_instance)
26
+ _logger.info("Role created successfully")
27
+ return cls(**created_role)
@@ -0,0 +1,10 @@
1
+ class Route:
2
+ def __init__(self, path, method, summary, responses, extensions):
3
+ self.path = path
4
+ self.method = method
5
+ self.summary = summary
6
+ self.responses = responses
7
+ self.extensions = extensions
8
+
9
+ def __str__(self):
10
+ return f"Path: {self.path}, Method: {self.method}, Summary: {self.summary}, Responses: {self.responses}, Extensions: {self.extensions}"
@@ -0,0 +1,5 @@
1
+ from . import AccessRights
2
+ from . import Model
3
+ from . import Openapi
4
+ from . import Role
5
+ from . import Route
@@ -0,0 +1,2 @@
1
+ from . import logger
2
+ from . import utils
@@ -0,0 +1,34 @@
1
+ import logging
2
+
3
+ class CustomFormatter(logging.Formatter):
4
+
5
+ purple = "\x1b[35;20m"
6
+ green = "\x1b[32;20m"
7
+ yellow = "\x1b[33;20m"
8
+ red = "\x1b[31;20m"
9
+ bold_red = "\x1b[31;1m"
10
+ reset = "\x1b[0m"
11
+ format = "%(asctime)s - (%(filename)s:%(lineno)d) - %(levelname)s - %(message)s "
12
+
13
+ FORMATS = {
14
+ logging.DEBUG: purple + format + reset,
15
+ logging.INFO: green + format + reset,
16
+ logging.WARNING: yellow + format + reset,
17
+ logging.ERROR: red + format + reset,
18
+ logging.CRITICAL: bold_red + format + reset
19
+ }
20
+
21
+ def format(self, record):
22
+ log_fmt = self.FORMATS.get(record.levelno, self.format)
23
+ formatter = logging.Formatter(log_fmt)
24
+ return formatter.format(record)
25
+
26
+ _logger = logging.getLogger("LAIA")
27
+ _logger.setLevel(logging.INFO)
28
+
29
+ ch = logging.StreamHandler()
30
+ ch.setLevel(logging.DEBUG)
31
+
32
+ ch.setFormatter(CustomFormatter())
33
+
34
+ _logger.addHandler(ch)
@@ -0,0 +1,41 @@
1
+ from typing import TypeVar
2
+ import subprocess
3
+ import re
4
+ from ..crud.crud import CRUD
5
+ from .logger import _logger
6
+
7
+ T = TypeVar('T', bound='BaseModel')
8
+
9
+ def create_models_file(input_file="openapi.yaml", output_file="model.py"):
10
+ # This function uses the datamodel-code-generator for generating the pydantic models given a openapi.yaml file.
11
+ # The generated file is modified so that the pydantic models extend the LaiaBaseModel, this is necessary for
12
+ # using the Laia library
13
+
14
+ subprocess.run(["datamodel-codegen", "--input", input_file, "--output", output_file], check=True)
15
+
16
+ import_statement = """
17
+ # modified by laia-gen-lib:
18
+
19
+ from laiagenlib.models.Model import LaiaBaseModel"""
20
+
21
+ with open(output_file, 'r') as f:
22
+ model_content = f.read()
23
+
24
+ lines = model_content.split('\n')
25
+ import_index = next((i for i, line in enumerate(lines) if "from __future__ import annotations" in line), None)
26
+
27
+ if import_index is not None:
28
+ lines.insert(import_index + 1, import_statement)
29
+
30
+ modified_content = '\n'.join(lines)
31
+ modified_content = re.sub(r'class\s+(\w+)\(BaseModel\):', r'class \1(LaiaBaseModel):', modified_content)
32
+
33
+ with open(output_file, 'w') as f:
34
+ f.write(modified_content)
35
+
36
+ print(f"File '{output_file}' created and modified.")
37
+
38
+ async def create_element(element: T, crud_instance: CRUD):
39
+ model_name = element.__class__.__name__.lower()
40
+
41
+ return await crud_instance.post_item(model_name, element)
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ from setuptools import find_packages, setup
2
+
3
+ setup(
4
+ name='laia-gen-lib',
5
+ packages=find_packages(),
6
+ version='0.1.0',
7
+ description='An AI application generator engine',
8
+ author='Me',
9
+ install_requires=['pymongo', 'pydantic', 'datamodel-code-generator'],
10
+ setup_requires=['pytest-runner'],
11
+ tests_require=['pytest', 'pymongo', 'pytest-asyncio'],
12
+ test_suite='tests',
13
+ )
File without changes
File without changes
@@ -0,0 +1,155 @@
1
+ import pytest
2
+ import pytest_asyncio
3
+ from pymongo import MongoClient
4
+ from laiagenlib.crud.crud_mongo_impl import CRUDMongoImpl
5
+ from laiagenlib.models.AccessRights import AccessRights
6
+ from laiagenlib.models.Model import LaiaBaseModel
7
+
8
+ class User(LaiaBaseModel):
9
+ description: str
10
+ age: int
11
+
12
+ @pytest.fixture
13
+ def in_memory_db():
14
+ client = MongoClient()
15
+ db = client["testdb"]
16
+ db.drop_collection("accessrights")
17
+ return db
18
+
19
+ @pytest_asyncio.fixture
20
+ async def crud_instance(in_memory_db):
21
+ return CRUDMongoImpl(in_memory_db)
22
+
23
+ class TestAccessRights:
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_model_name_different_that_model_raises_exception(self, crud_instance):
27
+ user_roles = ["admin"]
28
+ new_access_rights = {
29
+ "role": "owner",
30
+ "model": "drone",
31
+ "operations": {"create": 1, "read": 1},
32
+ "fields_create": {"description": 1},
33
+ "fields_edit": {},
34
+ "fields_visible": {"description": 1}
35
+ }
36
+
37
+ with pytest.raises(ValueError, match="Provided model name does not match the class model name"):
38
+ await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_create_access_rights_no_admin_permission_raises_exception(self, crud_instance):
42
+ user_roles = ["user"]
43
+ new_access_rights = {
44
+ "role": "userAdmin",
45
+ "model": "user",
46
+ "operations": {"create": 1, "read": 1},
47
+ "fields_create": {"description": 1},
48
+ "fields_edit": {},
49
+ "fields_visible": {"description": 1}
50
+ }
51
+
52
+ with pytest.raises(PermissionError, match="Only users with 'admin' role can create access rights"):
53
+ await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_create_access_rights_admin_permission(self, crud_instance):
57
+ user_roles = ["admin"]
58
+ new_access_rights = {
59
+ "role": "userAdmin",
60
+ "model": "user",
61
+ "operations": {"create": 1, "read": 1},
62
+ "fields_create": {"description": 1},
63
+ "fields_edit": {"name": 1},
64
+ "fields_visible": {"description": 1}
65
+ }
66
+
67
+ created_access_rights = await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
68
+
69
+ assert created_access_rights.role == "userAdmin"
70
+ assert created_access_rights.model == "user"
71
+
72
+ @pytest.mark.asyncio
73
+ async def test_create_access_rights_admin_permission(self, crud_instance):
74
+ user_roles = ["admin"]
75
+ new_access_rights = {
76
+ "role": "userAdmin",
77
+ "operations": {"create": 1, "read": 1},
78
+ "fields_create": {"description": 1},
79
+ "fields_edit": {"name": 1},
80
+ "fields_visible": {"description": 1}
81
+ }
82
+
83
+ with pytest.raises(ValueError, match="Missing required parameters"):
84
+ await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_create_access_rights_invalid_operations(self, crud_instance):
88
+ user_roles = ["admin"]
89
+ new_access_rights = {
90
+ "role": "userAdmin",
91
+ "model": "user",
92
+ "operations": {"invalid_op": 1},
93
+ "fields_create": {"description": 1},
94
+ "fields_edit": {"name": 1},
95
+ "fields_visible": {"description": 1}
96
+ }
97
+
98
+ with pytest.raises(ValueError, match="Invalid format for operation invalid_op"):
99
+ await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
100
+
101
+ @pytest.mark.asyncio
102
+ async def test_create_access_rights_invalid_fields_format(self, crud_instance):
103
+ user_roles = ["admin"]
104
+ new_access_rights = {
105
+ "role": "userAdmin",
106
+ "model": "user",
107
+ "operations": {"create": 1, "read": 1},
108
+ "fields_create": "invalid_format",
109
+ "fields_edit": {"name": 1},
110
+ "fields_visible": {"description": 1}
111
+ }
112
+
113
+ with pytest.raises(ValueError, match="Invalid format for fields_create"):
114
+ await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
115
+
116
+ @pytest.mark.asyncio
117
+ async def test_create_access_rights_invalid_field_name(self, crud_instance):
118
+ user_roles = ["admin"]
119
+ new_access_rights = {
120
+ "role": "userAdmin",
121
+ "model": "user",
122
+ "operations": {"create": 1, "read": 1},
123
+ "fields_create": {"invalid_field": 1},
124
+ "fields_edit": {"name": 1},
125
+ "fields_visible": {"description": 1}
126
+ }
127
+
128
+ with pytest.raises(ValueError, match="Invalid field invalid_field for fields_create"):
129
+ await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
130
+
131
+ @pytest.mark.asyncio
132
+ async def test_create_access_rights_already_exists(self, crud_instance):
133
+ user_roles = ["admin"]
134
+ existing_access_rights = {
135
+ "role": "userAdmin",
136
+ "model": "user",
137
+ "operations": {"create": 1, "read": 1},
138
+ "fields_create": {"description": 1},
139
+ "fields_edit": {"name": 1},
140
+ "fields_visible": {"description": 1}
141
+ }
142
+
143
+ await AccessRights.create(existing_access_rights, User, user_roles, crud_instance)
144
+
145
+ new_access_rights = {
146
+ "role": "userAdmin",
147
+ "model": "user",
148
+ "operations": {"create": 1, "read": 1},
149
+ "fields_create": {"description": 1},
150
+ "fields_edit": {"name": 1},
151
+ "fields_visible": {"description": 1}
152
+ }
153
+
154
+ with pytest.raises(ValueError, match="AccessRights with the same role and model already exists"):
155
+ await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
@@ -0,0 +1,258 @@
1
+ import pytest
2
+ import pytest_asyncio
3
+ from pymongo import MongoClient
4
+ from laiagenlib.crud.crud_mongo_impl import CRUDMongoImpl
5
+ from laiagenlib.models.Model import LaiaBaseModel
6
+ from laiagenlib.models.AccessRights import AccessRights
7
+ from laiagenlib.utils.logger import _logger
8
+
9
+ class User(LaiaBaseModel):
10
+ description: str
11
+ age: int
12
+
13
+ class Drone(LaiaBaseModel):
14
+ description: str
15
+ weight: float
16
+ max_altitude: float
17
+ max_speed: float
18
+
19
+ @pytest.fixture
20
+ def in_memory_db():
21
+ client = MongoClient()
22
+ db = client["testdb"]
23
+ db.drop_collection("user")
24
+ db.drop_collection("drone")
25
+ db.drop_collection("accessrights")
26
+ return db
27
+
28
+ @pytest_asyncio.fixture
29
+ async def crud_instance(in_memory_db):
30
+ return CRUDMongoImpl(in_memory_db)
31
+
32
+ class TestModel:
33
+
34
+ @pytest.mark.asyncio
35
+ async def test_model_created_by_admin(self, crud_instance):
36
+ user_roles = ["admin"]
37
+ new_user = {
38
+ "name": "alba",
39
+ "description": "This is a description",
40
+ "age": 21
41
+ }
42
+ await User.create(new_user, User, user_roles, crud_instance)
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_create_element_missing_parameters_raises_exception(self, crud_instance):
46
+ user_roles = ["admin"]
47
+ new_user = {
48
+ "name": "alba",
49
+ "description": "This is a description",
50
+ }
51
+
52
+ with pytest.raises(ValueError, match="Missing required parameters"):
53
+ await User.create(new_user, User, user_roles, crud_instance)
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_create_element_missing_access_rights(self, crud_instance):
57
+ user_roles = ["userAdmin"]
58
+ new_user = {
59
+ "name": "alba",
60
+ "description": "This is a description",
61
+ "age": 21
62
+ }
63
+
64
+ with pytest.raises(PermissionError, match="None of the roles have sufficient permissions for operation 'create' on model 'user'"):
65
+ await User.create(new_user, User, user_roles, crud_instance)
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_create_element_missing_access_rights_create_operation(self, crud_instance):
69
+ new_access_rights = {
70
+ "role": "userAdmin",
71
+ "model": "user",
72
+ "operations": {"read": 1},
73
+ "fields_create": {"description": 1},
74
+ "fields_edit": {"name": 1},
75
+ "fields_visible": {"description": 1}
76
+ }
77
+
78
+ await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
79
+
80
+ user_roles = ["userAdmin"]
81
+ new_user = {
82
+ "name": "alba",
83
+ "description": "This is a description",
84
+ "age": 21
85
+ }
86
+
87
+ with pytest.raises(PermissionError, match="None of the roles have sufficient permissions for operation 'create' on model 'user'"):
88
+ await User.create(new_user, User, user_roles, crud_instance)
89
+
90
+ @pytest.mark.asyncio
91
+ async def test_create_element_missing_access_rights_specific_field(self, crud_instance):
92
+ new_access_rights = {
93
+ "role": "userAdmin",
94
+ "model": "user",
95
+ "operations": {"create": 1, "read": 1},
96
+ "fields_create": {"name": 1, "description": 1},
97
+ "fields_edit": {"name": 1},
98
+ "fields_visible": {"description": 1}
99
+ }
100
+
101
+ await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
102
+
103
+ user_roles = ["userAdmin"]
104
+ new_user = {
105
+ "name": "alba",
106
+ "description": "This is a description",
107
+ "age": 21
108
+ }
109
+
110
+ with pytest.raises(PermissionError, match="Insufficient permissions to create the field 'age' in any role."):
111
+ await User.create(new_user, User, user_roles, crud_instance)
112
+
113
+ @pytest.mark.asyncio
114
+ async def test_create_element_with_different_role(self, crud_instance):
115
+ new_access_rights1 = {
116
+ "role": "userAdmin1",
117
+ "model": "user",
118
+ "operations": {"create": 1, "read": 1},
119
+ "fields_create": {"name": 1, "description": 1},
120
+ "fields_edit": {"name": 1},
121
+ "fields_visible": {"description": 1}
122
+ }
123
+
124
+ new_access_rights2 = {
125
+ "role": "userAdmin2",
126
+ "model": "user",
127
+ "operations": {"create": 1, "read": 1},
128
+ "fields_create": {"name": 1, "description": 1, "age": 1},
129
+ "fields_edit": {"name": 1},
130
+ "fields_visible": {"description": 1}
131
+ }
132
+
133
+
134
+ await AccessRights.create(new_access_rights1, User, ["admin"], crud_instance)
135
+ await AccessRights.create(new_access_rights2, User, ["admin"], crud_instance)
136
+
137
+ user_roles = ["userAdmin1", "userAdmin2"]
138
+ new_user = {
139
+ "name": "alba",
140
+ "description": "This is a description",
141
+ "age": 21
142
+ }
143
+
144
+ user = await User.create(new_user, User, user_roles, crud_instance)
145
+
146
+ assert user is not None
147
+ assert user["description"] == "This is a description"
148
+
149
+ @pytest.mark.asyncio
150
+ async def test_model_updated_by_admin(self, crud_instance):
151
+ user_roles = ["admin"]
152
+ new_user = {
153
+ "name": "alba",
154
+ "description": "This is a description",
155
+ "age": 21
156
+ }
157
+
158
+ user = await User.create(new_user, User, user_roles, crud_instance)
159
+
160
+ updated_user = await User.update(user["id"], {"name": "blanca"}, User, user_roles, crud_instance)
161
+
162
+ assert updated_user is not None
163
+ assert updated_user["name"] == "blanca"
164
+ assert updated_user["description"] == user["description"]
165
+ assert updated_user["age"] == user["age"]
166
+ assert updated_user["id"] == user["id"]
167
+
168
+ @pytest.mark.asyncio
169
+ async def test_read_element_missing_access_rights_read_operation(self, crud_instance):
170
+ new_access_rights = {
171
+ "role": "userAdmin",
172
+ "model": "user",
173
+ "operations": {"create": 1, "update": 1},
174
+ "fields_create": {"name": 1, "description": 1},
175
+ "fields_edit": {"name": 1},
176
+ "fields_visible": {"description": 1}
177
+ }
178
+
179
+ await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
180
+
181
+ user_roles = ["userAdmin"]
182
+ new_user = {
183
+ "name": "alba",
184
+ "description": "This is a description",
185
+ "age": 21
186
+ }
187
+
188
+ user = await User.create(new_user, User, ["admin"], crud_instance)
189
+
190
+ with pytest.raises(PermissionError, match="None of the roles have sufficient permissions for operation 'read' on model 'user'"):
191
+ await User.read(user["id"], User, user_roles, crud_instance)
192
+
193
+ @pytest.mark.asyncio
194
+ async def test_read_element_missing_access_rights_specific_field(self, crud_instance):
195
+ new_access_rights = {
196
+ "role": "userAdmin",
197
+ "model": "user",
198
+ "operations": {"create": 1, "read": 1, "update": 1},
199
+ "fields_create": {"name": 1, "description": 1},
200
+ "fields_edit": {"name": 1},
201
+ "fields_visible": {"description": 1}
202
+ }
203
+
204
+ await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
205
+
206
+ user_roles = ["userAdmin"]
207
+ new_user = {
208
+ "name": "alba",
209
+ "description": "This is a description",
210
+ "age": 21
211
+ }
212
+
213
+ user = await User.create(new_user, User, ["admin"], crud_instance)
214
+
215
+ user_read = await User.read(user["id"], User, user_roles, crud_instance)
216
+
217
+ assert user_read == {"description": "This is a description"}
218
+
219
+ @pytest.mark.asyncio
220
+ async def test_delete_element(self, crud_instance):
221
+ user_roles = ["admin"]
222
+ new_user = {
223
+ "name": "alba",
224
+ "description": "This is a description",
225
+ "age": 21
226
+ }
227
+
228
+ user = await User.create(new_user, User, user_roles, crud_instance)
229
+
230
+ await User.delete(user["id"], User, user_roles, crud_instance)
231
+
232
+ with pytest.raises(ValueError, match=f"user with ID {user['id']} not found"):
233
+ await User.read(user["id"], User, user_roles, crud_instance)
234
+
235
+ @pytest.mark.asyncio
236
+ async def test_delete_element_missing_access_rights_delete_operation(self, crud_instance):
237
+ new_access_rights = {
238
+ "role": "userAdmin",
239
+ "model": "user",
240
+ "operations": {"create": 1, "update": 1},
241
+ "fields_create": {"name": 1, "description": 1},
242
+ "fields_edit": {"name": 1},
243
+ "fields_visible": {"description": 1}
244
+ }
245
+
246
+ await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
247
+
248
+ user_roles = ["userAdmin"]
249
+ new_user = {
250
+ "name": "alba",
251
+ "description": "This is a description",
252
+ "age": 21
253
+ }
254
+
255
+ user = await User.create(new_user, User, ["admin"], crud_instance)
256
+
257
+ with pytest.raises(PermissionError, match="None of the roles have sufficient permissions for operation 'delete' on model 'user'"):
258
+ await User.delete(user["id"], User, user_roles, crud_instance)
@@ -0,0 +1,26 @@
1
+ import pytest
2
+ import re
3
+ import os
4
+ from laiagenlib.models.Openapi import OpenAPI
5
+
6
+ @pytest.fixture
7
+ def test_files(request):
8
+ test_dir = os.path.dirname(os.path.abspath(request.module.__file__))
9
+ input_file1 = os.path.join(test_dir, "..", "openapi_files", "api.yaml")
10
+ input_file2 = os.path.join(test_dir, "..", "openapi_files", "api2.yaml")
11
+ input_file3 = os.path.join(test_dir, "..", "openapi_files", "api3.yaml")
12
+ yield input_file1, input_file2, input_file3
13
+
14
+ def test_create_models_file(test_files):
15
+ input_file1, input_file2, input_file3 = test_files
16
+
17
+ parser = OpenAPI(input_file1)
18
+ assert len(parser.routes) == 8
19
+
20
+ parser2 = OpenAPI(input_file2)
21
+ assert len(parser2.routes) == 6
22
+
23
+ parser3 = OpenAPI(input_file3)
24
+ assert len(parser3.routes) == 1
25
+
26
+
@@ -0,0 +1,58 @@
1
+ import pytest
2
+ import pytest_asyncio
3
+ from pymongo import MongoClient
4
+ from laiagenlib.crud.crud_mongo_impl import CRUDMongoImpl
5
+ from laiagenlib.models.Role import Role
6
+
7
+ @pytest.fixture
8
+ def in_memory_db():
9
+ client = MongoClient()
10
+ db = client["testdb"]
11
+ db.drop_collection("role")
12
+ return db
13
+
14
+ @pytest_asyncio.fixture
15
+ async def crud_instance(in_memory_db):
16
+ return CRUDMongoImpl(in_memory_db)
17
+
18
+ class TestRole:
19
+
20
+ @pytest.mark.asyncio
21
+ async def test_create_role_without_name_raises_exception(self, crud_instance):
22
+ user_roles = ["admin"]
23
+ new_role = {"something": "test_role"}
24
+
25
+ with pytest.raises(ValueError, match="Missing required parameter: name"):
26
+ await Role.create(new_role, user_roles, crud_instance)
27
+
28
+ @pytest.mark.asyncio
29
+ async def test_create_role_admin_permission(self, crud_instance):
30
+ user_roles = ["admin"]
31
+ new_role = {"name": "test_role"}
32
+
33
+ created_role = await Role.create(new_role, user_roles, crud_instance)
34
+
35
+ assert created_role.name == "test_role"
36
+
37
+ found_roles, _ = await crud_instance.get_items("role", filters={"name": "test_role"})
38
+
39
+ assert len(found_roles) == 1
40
+ assert found_roles[0]["name"] == "test_role"
41
+
42
+ @pytest.mark.asyncio
43
+ async def test_create_role_different_permission(self, crud_instance):
44
+ user_roles = ["user"]
45
+ new_role = {"name": "test_role"}
46
+
47
+ with pytest.raises(PermissionError, match="Only users with 'admin' role can create new roles"):
48
+ await Role.create(new_role, user_roles, crud_instance)
49
+
50
+ @pytest.mark.asyncio
51
+ async def test_create_role_duplicate_name(self, crud_instance):
52
+ user_roles = ["admin"]
53
+ new_role = {"name": "test_role"}
54
+
55
+ await Role.create(new_role, user_roles, crud_instance)
56
+
57
+ with pytest.raises(ValueError, match=f"Role with name '{new_role['name']}' already exists"):
58
+ await Role.create(new_role, user_roles, crud_instance)