Skip to content

Commit

Permalink
[Feature] Implement support to export project config (#171)
Browse files Browse the repository at this point in the history
* Implement support to export project config

* Fix mypy

* Changed the returned schema and service endpoint name

* Added import project config

* Apply suggestions from code review

Co-authored-by: Carolina Lopes <[email protected]>

* code review

---------

Co-authored-by: Carolina Lopes <[email protected]>
  • Loading branch information
rquidute and ccruzagralopes authored Dec 2, 2024
1 parent 56a9353 commit 913752e
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 4 deletions.
75 changes: 75 additions & 0 deletions app/api/api_v1/endpoints/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
import json
from http import HTTPStatus
from typing import List, Sequence, Union

from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import ValidationError, parse_obj_as
from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import flag_modified

Expand Down Expand Up @@ -328,3 +332,74 @@ def __persist_pics_update(db: Session, project: Project) -> Project:
db.commit()
db.refresh(project)
return project


@router.get("/{id}/export", response_model=schemas.ProjectCreate)
def export_project_config(
*,
db: Session = Depends(get_db),
id: int,
) -> JSONResponse:
"""
Exports the project config by id.
Args:
id (int): project id
Raises:
HTTPException: if no project exists for provided project id
Returns:
JSONResponse: json representation of the project with the informed project id
"""
# Retrieve project by project_id using schemas.ProjectCreate schema
project = schemas.ProjectCreate(**__project(db=db, id=id).__dict__)

options: dict = {"media_type": "application/json"}
filename = f"{project.name}-project-config.json"
options["headers"] = {"Content-Disposition": f'attachment; filename="{filename}"'}

return JSONResponse(
jsonable_encoder(project),
**options,
)


@router.post("/import", response_model=schemas.Project)
def importproject_config(
*,
db: Session = Depends(get_db),
import_file: UploadFile = File(...),
) -> models.Project:
"""
Imports the project config
Args:
import_file : The project config file to be imported
Raises:
ValidationError: if the imported project config contains invalid information
Returns:
Project: newly created project record
"""

file_content = import_file.file.read().decode("utf-8")
file_dict = json.loads(file_content)

try:
imported_project: schemas.ProjectCreate = parse_obj_as(
schemas.ProjectCreate, file_dict
)
except ValidationError as error:
raise HTTPException(
status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=str(error)
)

try:
return crud.project.create(db=db, obj_in=imported_project)
except TestEnvironmentConfigError as e:
raise HTTPException(
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
detail=str(e),
)
88 changes: 84 additions & 4 deletions app/tests/api/api_v1/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
#
# flake8: noqa
# Ignore flake8 check for this file
import json
from http import HTTPStatus
from io import BytesIO
from pathlib import Path
from typing import Any

Expand All @@ -24,10 +26,9 @@
from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app import crud
from app import crud, models, schemas
from app.core.config import settings
from app.default_environment_config import default_environment_config
from app.models.project import Project
from app.tests.utils.project import (
create_random_project,
create_random_project_archived,
Expand Down Expand Up @@ -65,6 +66,49 @@
},
}

project_json_data = {
"name": "New Project IMPORTED",
"config": {
"test_parameters": None,
"network": {
"wifi": {"ssid": "testharness", "password": "wifi-password"},
"thread": {
"rcp_serial_path": "/dev/ttyACM0",
"rcp_baudrate": 115200,
"on_mesh_prefix": "fd11:22::/64",
"network_interface": "eth0",
"dataset": {
"channel": "15",
"panid": "0x1234",
"extpanid": "1111111122222222",
"networkkey": "00112233445566778899aabbccddeeff",
"networkname": "DEMO",
},
"otbr_docker_image": None,
},
},
"dut_config": {
"discriminator": "3840",
"setup_code": "20202021",
"pairing_mode": "onnetwork",
"chip_timeout": None,
"chip_use_paa_certs": False,
"trace_log": True,
},
},
"pics": {
"clusters": {
"Access Control cluster": {
"name": "Test PICS",
"items": {
"ACL.S": {"number": "PICS.S", "enabled": False},
"ACL.C": {"number": "PICS.C", "enabled": True},
},
}
}
},
}


def test_create_project_default_config(client: TestClient) -> None:
data: dict[str, Any] = {"name": "Foo"}
Expand Down Expand Up @@ -151,7 +195,7 @@ def test_read_project(client: TestClient, db: Session) -> None:
def test_read_multiple_project(client: TestClient, db: Session) -> None:
project1 = create_random_project(db, config={})
project2 = create_random_project(db, config={})
limit = db.scalar(select(func.count(Project.id))) or 0
limit = db.scalar(select(func.count(models.Project.id))) or 0
response = client.get(
f"{settings.API_V1_STR}/projects?limit={limit}",
)
Expand All @@ -164,7 +208,7 @@ def test_read_multiple_project(client: TestClient, db: Session) -> None:

def test_read_multiple_project_by_archived(client: TestClient, db: Session) -> None:
archived = create_random_project_archived(db, config={})
limit = db.scalar(select(func.count(Project.id))) or 0
limit = db.scalar(select(func.count(models.Project.id))) or 0

response = client.get(
f"{settings.API_V1_STR}/projects?limit={limit}",
Expand Down Expand Up @@ -371,3 +415,39 @@ def test_applicable_test_cases_empty_pics(client: TestClient, db: Session) -> No
# the project is created with empty pics
# expected value: applicable_test_cases == 0
assert len(content["test_cases"]) == 0


def test_export_project(client: TestClient, db: Session) -> None:
project = create_random_project_with_pics(db=db, config={})
project_create_schema = schemas.ProjectCreate(**project.__dict__)
# retrieve the project config
response = client.get(
f"{settings.API_V1_STR}/projects/{project.id}/export",
)

validate_json_response(
response=response,
expected_status_code=HTTPStatus.OK,
expected_content=jsonable_encoder(project_create_schema),
)


def test_import_project(client: TestClient, db: Session) -> None:
imported_file_content = json.dumps(project_json_data).encode("utf-8")
data = BytesIO(imported_file_content)

files = {
"import_file": (
"project.json",
data,
"multipart/form-data",
)
}

response = client.post(f"{settings.API_V1_STR}/projects/import", files=files)

validate_json_response(
response=response,
expected_status_code=HTTPStatus.OK,
expected_content=project_json_data,
)

0 comments on commit 913752e

Please sign in to comment.