Coverage for src\cognitivefactory\interactive_clustering_gui\app.py: 100.00%
635 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-23 17:54 +0100
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-23 17:54 +0100
1# -*- coding: utf-8 -*-
3"""
4* Name: cognitivefactory.interactive_clustering_gui.app
5* Description: Definition of FastAPI application and routes for interactive clustering graphical user interface.
6* Author: Erwan Schild
7* Created: 22/10/2021
8* Licence: CeCILL-C License v1.0 (https://cecill.info/licences.fr.html)
9"""
11# ==============================================================================
12# IMPORT PYTHON DEPENDENCIES
13# ==============================================================================
15import html
16import json
17import os
18import pathlib
19import shutil
20from datetime import datetime
21from typing import Any, Callable, Dict, List, Optional, Tuple
23import pandas as pd
24from dateutil import tz
25from fastapi import (
26 BackgroundTasks,
27 Body,
28 FastAPI,
29 File,
30 HTTPException,
31 Path,
32 Query,
33 Request,
34 Response,
35 UploadFile,
36 status,
37)
38from fastapi.middleware.cors import CORSMiddleware
39from fastapi.responses import FileResponse # HTMLResponse
40from fastapi.staticfiles import StaticFiles
41from fastapi.templating import Jinja2Templates
42from filelock import FileLock
43from importlib_metadata import version
44from prometheus_client import Gauge
45from prometheus_fastapi_instrumentator import Instrumentator, metrics
46from zipp import zipfile
48from cognitivefactory.interactive_clustering_gui import backgroundtasks
49from cognitivefactory.interactive_clustering_gui.models.queries import (
50 ConstraintsSortOptions,
51 ConstraintsValues,
52 TextsSortOptions,
53)
54from cognitivefactory.interactive_clustering_gui.models.settings import (
55 ClusteringSettingsModel,
56 ICGUISettings,
57 PreprocessingSettingsModel,
58 SamplingSettingsModel,
59 VectorizationSettingsModel,
60 default_ClusteringSettingsModel,
61 default_PreprocessingSettingsModel,
62 default_SamplingSettingsModel,
63 default_VectorizationSettingsModel,
64)
65from cognitivefactory.interactive_clustering_gui.models.states import ICGUIStates, get_ICGUIStates_details
67# ==============================================================================
68# CONFIGURE FASTAPI APPLICATION
69# ==============================================================================
71# Create an FastAPI instance of Interactive Clustering Graphical User Interface.
72app = FastAPI(
73 title="Interactive Clustering GUI",
74 description="Web application for Interactive Clustering methodology",
75 version=version("cognitivefactory-interactive-clustering-gui"),
76)
78# Add Cross-Origin Resource Sharing
79app.add_middleware(
80 CORSMiddleware,
81 allow_origins=[ # origins for which we allow CORS (Cross-Origin Resource Sharing).
82 "http://localhost:8000", # locally served docs.
83 # "https://cognitivefactory.github.io/interactive-clustering-gui/", # deployed docs.
84 # you could add `http://your_vm_hostname:8000`.
85 ],
86)
88# Mount `css`, `javascript` and `resources` files.
89app.mount("/css", StaticFiles(directory=pathlib.Path(__file__).parent / "css"), name="css")
90app.mount("/js", StaticFiles(directory=pathlib.Path(__file__).parent / "js"), name="js")
91app.mount("/resources", StaticFiles(directory=pathlib.Path(__file__).parent / "resources"), name="resources")
93# Define `DATA_DIRECTORY` (where data are stored).
94DATA_DIRECTORY = pathlib.Path(os.environ.get("DATA_DIRECTORY", ".data"))
95DATA_DIRECTORY.mkdir(parents=True, exist_ok=True)
98# ==============================================================================
99# CONFIGURE JINJA2 TEMPLATES
100# ==============================================================================
102# Define HTML templates to render.
103templates = Jinja2Templates(directory=pathlib.Path(__file__).parent / "html")
106# Define function to convert timestamp to date.
107def timestamp_to_date(timestamp: float, timezone_str: str = "Europe/Paris") -> str:
108 """
109 From timestamp to date.
111 Args:
112 timestamp (float): The timstamp to convert.
113 timezone_str (str, optional): The time zone. Defaults to `"Europe/Paris"`.
115 Returns:
116 str: The requested date.
117 """
118 timezone = tz.gettz(timezone_str)
119 return datetime.fromtimestamp(timestamp, timezone).strftime("%d/%m/%Y")
122templates.env.filters["timestamp_to_date"] = timestamp_to_date
125# Define function to convert timestamp to hour.
126def timestamp_to_hour(timestamp: float, timezone_str: str = "Europe/Paris") -> str:
127 """
128 From timestamp to hours.
130 Args:
131 timestamp (float): The timstamp to convert.
132 timezone_str (str, optional): The time zone. Defaults to `"Europe/Paris"`.
134 Returns:
135 str: The requested hour.
136 """
137 timezone = tz.gettz(timezone_str)
138 return datetime.fromtimestamp(timestamp, timezone).strftime("%H:%M:%S")
141templates.env.filters["timestamp_to_hour"] = timestamp_to_hour
144# Define function to get previous key in a dictionary.
145def get_previous_key(key: str, dictionary: Dict[str, Any]) -> Optional[str]:
146 """
147 Get previous key in a dictionary.
149 Args:
150 key (str): The current key.
151 dictionary (Dict[str, Any]): The dictionary.
153 Returns:
154 Optional[str]: The previous key.
155 """
156 list_of_keys: List[str] = list(dictionary.keys())
157 if key in list_of_keys:
158 previous_key_index: int = list_of_keys.index(key) - 1
159 return list_of_keys[previous_key_index] if 0 <= previous_key_index else None
160 return None
163templates.env.filters["get_previous_key"] = get_previous_key
166# Define function to get next key in a dictionary.
167def get_next_key(key: str, dictionary: Dict[str, Any]) -> Optional[str]:
168 """
169 Get next key in a dictionary.
171 Args:
172 key (str): The current key.
173 dictionary (Dict[str, Any]): The dictionary.
175 Returns:
176 Optional[str]: The next key.
177 """
178 list_of_keys: List[str] = list(dictionary.keys())
179 if key in list_of_keys:
180 next_key_index: int = list_of_keys.index(key) + 1
181 return list_of_keys[next_key_index] if next_key_index < len(list_of_keys) else None
182 return None
185templates.env.filters["get_next_key"] = get_next_key
188# ==============================================================================
189# CONFIGURE FASTAPI METRICS
190# ==============================================================================
193def prometheus_disk_usage() -> Callable[[metrics.Info], None]:
194 """
195 Define a metric of disk usage.
197 Returns:
198 Callable[[metrics.Info], None]: instrumentation.
199 """
200 gaugemetric = Gauge(
201 "disk_usage",
202 "The disk usage in %",
203 )
205 def instrumentation(info: metrics.Info) -> None: # noqa: WPS430 (nested function)
206 total, used, _ = shutil.disk_usage(DATA_DIRECTORY)
207 gaugemetric.set(used * 100 / total)
209 return instrumentation
212# Define application instrumentator and add metrics.
213instrumentator = Instrumentator()
214instrumentator.add(metrics.default())
215instrumentator.add(prometheus_disk_usage())
216instrumentator.instrument(app)
217instrumentator.expose(app)
220# ==============================================================================
221# DEFINE STATE ROUTES FOR APPLICATION STATE
222# ==============================================================================
225###
226### STATE: Startup event.
227###
228@app.on_event("startup")
229async def startup() -> None: # pragma: no cover
230 """Startup event."""
232 # Initialize ready state.
233 app.state.ready = False
235 # Apply database connection, long loading, etc.
237 # Update ready state when done.
238 app.state.ready = True
241###
242### STATE: Check if app is ready.
243###
244@app.get(
245 "/ready",
246 tags=["app state"],
247 status_code=status.HTTP_200_OK,
248)
249async def ready() -> Response: # pragma: no cover
250 """
251 Tell if the API is ready.
253 Returns:
254 An HTTP response with either 200 or 503 codes.
255 """
257 # Return 200_OK if ready.
258 if app.state.ready:
259 return Response(status_code=status.HTTP_200_OK)
261 # Return 503_SERVICE_UNAVAILABLE otherwise.
262 return Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
265###
266### STATE: Check if app is alive.
267###
268@app.get(
269 "/alive",
270 tags=["app state"],
271 status_code=status.HTTP_200_OK,
272)
273async def alive() -> Response: # pragma: no cover
274 """
275 Tell if the API is alive/healthy.
277 Returns:
278 Response: An HTTP response with either 200 or 503 codes.
279 """
281 try:
282 # Assert the volume can be reached.
283 pathlib.Path(DATA_DIRECTORY / ".available").touch()
284 # Or anything else asserting the API is healthy.
285 except OSError:
286 return Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
287 return Response(status_code=status.HTTP_200_OK)
290# ==============================================================================
291# DEFINE ROUTES FOR HOME AND DOCUMENTATION
292# ==============================================================================
295###
296### ROUTE: Get HTML welcome page with projects listings.
297###
298@app.get(
299 "/",
300 tags=["Home and Documentation"],
301 response_class=Response,
302 status_code=status.HTTP_200_OK,
303)
304async def get_html_welcome_page(
305 request: Request,
306) -> Response:
307 """
308 Define HTML welcome page with projects listings.
310 Args:
311 request (Request): The request context.
313 Returns:
314 Response: The requested page.
315 """
317 # Return HTML welcome page.
318 return templates.TemplateResponse(
319 name="welcome.html",
320 context={
321 "request": request,
322 # Get projects and their description.
323 "projects": {
324 project_id: {
325 "metadata": (await get_metadata(project_id=project_id))["metadata"],
326 "status": (await get_status(project_id=project_id))["status"],
327 }
328 for project_id in (await get_projects())
329 },
330 },
331 status_code=status.HTTP_200_OK,
332 )
335###
336### ROUTE: Get HTML help page.
337###
338@app.get(
339 "/gui/help",
340 tags=["Home and Documentation"],
341 response_class=Response,
342 status_code=status.HTTP_200_OK,
343)
344async def get_html_help_page(
345 request: Request,
346) -> Response:
347 """
348 Get HTML help page.
350 Args:
351 request (Request): The request context.
353 Returns:
354 Response: The requested page.
355 """
357 # Return HTML help page.
358 return templates.TemplateResponse(
359 name="help.html",
360 context={
361 "request": request,
362 },
363 status_code=status.HTTP_200_OK,
364 )
367# ==============================================================================
368# DEFINE ROUTES FOR PROJECT MANAGEMENT
369# ==============================================================================
372###
373### ROUTE: Get the list of existing project IDs.
374###
375@app.get(
376 "/api/projects",
377 tags=["Projects"],
378 status_code=status.HTTP_200_OK,
379)
380async def get_projects() -> List[str]:
381 """
382 Get the list of existing project IDs.
383 (A project is represented by a subfolder in `.data` folder.)
385 Returns:
386 List[str]: The list of existing project IDs.
387 """
389 # Return the list of project IDs.
390 return [project_id for project_id in os.listdir(DATA_DIRECTORY) if os.path.isdir(DATA_DIRECTORY / project_id)]
393###
394### ROUTE: Create a project.
395###
396@app.post(
397 "/api/projects",
398 tags=["Projects"],
399 status_code=status.HTTP_201_CREATED,
400)
401async def create_project(
402 project_name: str = Query(
403 ...,
404 description="The name of the project. Should not be an empty string.",
405 min_length=3,
406 max_length=64,
407 ),
408 dataset_file: UploadFile = File(
409 ...,
410 description="The dataset file to load. Use a `.csv` (`;` separator) or `.xlsx` file. Please use a list of texts in the first column, without header, with encoding 'utf-8'.",
411 # TODO: max_size="8MB",
412 ),
413) -> Dict[str, Any]:
414 """
415 Create a project.
417 Args:
418 project_name (str): The name of the project. Should not be an empty string.
419 dataset_file (UploadFile): The dataset file to load. Use a `.csv` (`;` separator) or `.xlsx` file. Please use a list of texts in the first column, without header, with encoding 'utf-8'.
421 Raises:
422 HTTPException: Raises `HTTP_400_BAD_REQUEST` if parameters `project_name` or `dataset_file` are invalid.
424 Returns:
425 Dict[str, Any]: A dictionary that contains the ID of the created project.
426 """
428 # Define the new project ID.
429 current_timestamp: float = datetime.now().timestamp()
430 current_project_id: str = str(int(current_timestamp * 10**6))
432 # Check project name.
433 if project_name.strip() == "":
434 raise HTTPException(
435 status_code=status.HTTP_400_BAD_REQUEST,
436 detail="The project name '{project_name_str}' is invalid.".format(
437 project_name_str=str(project_name),
438 ),
439 )
441 # Initialize variable to store loaded dataset.
442 list_of_texts: List[str] = []
444 # Load dataset: Case of `.csv` with `;` separator.
445 if dataset_file.content_type in {"text/csv", "application/vnd.ms-excel"}:
446 # "text/csv" == ".csv"
447 # "application/vnd.ms-excel" == ".xls"
448 try: # noqa: WPS229 # Found too long `try` body length
449 dataset_csv: pd.Dataframe = pd.read_csv(
450 filepath_or_buffer=dataset_file.file,
451 sep=";",
452 header=None, # No header expected in the csv file.
453 )
454 list_of_texts = dataset_csv[dataset_csv.columns[0]].tolist()
455 except Exception:
456 raise HTTPException(
457 status_code=status.HTTP_400_BAD_REQUEST,
458 detail="The dataset file is invalid. `.csv` file, with `;` separator, must contain a list of texts in the first column, with no header.",
459 )
460 # Load dataset: Case of `.xlsx`.
461 elif dataset_file.content_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
462 # "application/vnd.ms-excel" == ".xlsx"
463 try: # noqa: WPS229 # Found too long `try` body length
464 dataset_xlsx: pd.Dataframe = pd.read_excel(
465 io=dataset_file.file.read(),
466 engine="openpyxl",
467 header=None, # No header expected in the xlsx file.
468 )
469 list_of_texts = dataset_xlsx[dataset_xlsx.columns[0]].tolist()
470 except Exception:
471 raise HTTPException(
472 status_code=status.HTTP_400_BAD_REQUEST,
473 detail="The dataset file is invalid. `.xlsx` file must contain a list of texts in the first column, with no header.",
474 )
476 # Load dataset: case of not supported type.
477 else:
478 raise HTTPException(
479 status_code=status.HTTP_400_BAD_REQUEST,
480 detail="The file type '{dataset_file_type}' is not supported. Please use '.csv' (`;` separator) or '.xlsx' file.".format(
481 dataset_file_type=str(dataset_file.content_type),
482 ),
483 )
485 # Create the directory and subdirectories of the new project.
486 os.mkdir(DATA_DIRECTORY / current_project_id)
488 # Initialize storage of metadata.
489 with open(DATA_DIRECTORY / current_project_id / "metadata.json", "w") as metadata_fileobject:
490 json.dump(
491 {
492 "project_id": current_project_id,
493 "project_name": str(project_name.strip()),
494 "creation_timestamp": current_timestamp,
495 },
496 metadata_fileobject,
497 indent=4,
498 )
500 # Initialize storage of status.
501 with open(DATA_DIRECTORY / current_project_id / "status.json", "w") as status_fileobject:
502 json.dump(
503 {
504 "iteration_id": 0, # Use string format for JSON serialization in dictionaries.
505 "state": ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION,
506 "task": None, # "progression", "detail".
507 },
508 status_fileobject,
509 indent=4,
510 )
512 # Initialize storage of texts.
513 with open(DATA_DIRECTORY / current_project_id / "texts.json", "w") as texts_fileobject:
514 json.dump(
515 {
516 str(i): {
517 "text_original": str(text), # Will never be changed.
518 "text": str(text), # Can be change by renaming.
519 "text_preprocessed": str(text), # Will be preprocessed during `Modelizationpdate` task.
520 "is_deleted": False,
521 }
522 for i, text in enumerate(list_of_texts)
523 },
524 texts_fileobject,
525 indent=4,
526 )
528 # Initialize storage of constraints.
529 with open(DATA_DIRECTORY / current_project_id / "constraints.json", "w") as constraints_fileobject:
530 json.dump(
531 {}, # Dict[str, Any]
532 constraints_fileobject,
533 indent=4,
534 )
536 # Initialize storage of modelization inference assignations.
537 with open(DATA_DIRECTORY / current_project_id / "modelization.json", "w") as modelization_fileobject:
538 json.dump(
539 {str(i): {"MUST_LINK": [str(i)], "CANNOT_LINK": [], "COMPONENT": i} for i in range(len(list_of_texts))},
540 modelization_fileobject,
541 indent=4,
542 )
544 # Initialize settings storage.
545 with open(DATA_DIRECTORY / current_project_id / "settings.json", "w") as settings_fileobject:
546 json.dump(
547 {
548 "0": {
549 "preprocessing": default_PreprocessingSettingsModel().to_dict(),
550 "vectorization": default_VectorizationSettingsModel().to_dict(),
551 "clustering": default_ClusteringSettingsModel().to_dict(),
552 },
553 },
554 settings_fileobject,
555 indent=4,
556 )
558 # Initialize storage of sampling results.
559 with open(DATA_DIRECTORY / current_project_id / "sampling.json", "w") as sampling_fileobject:
560 json.dump({}, sampling_fileobject, indent=4) # Dict[str, List[str]]
562 # Initialize storage of clustering results.
563 with open(DATA_DIRECTORY / current_project_id / "clustering.json", "w") as clustering_fileobject:
564 json.dump({}, clustering_fileobject, indent=4) # Dict[str, Dict[str, str]]
566 # Return the ID of the created project.
567 return {
568 "project_id": current_project_id,
569 "detail": (
570 "The project with name '{project_name_str}' has been created. It has the id '{project_id_str}'.".format(
571 project_name_str=str(project_name),
572 project_id_str=str(current_project_id),
573 )
574 ),
575 }
578###
579### ROUTE: Delete a project.
580###
581@app.delete(
582 "/api/projects/{project_id}",
583 tags=["Projects"],
584 status_code=status.HTTP_202_ACCEPTED,
585)
586async def delete_project(
587 project_id: str = Path(
588 ...,
589 description="The ID of the project to delete.",
590 ),
591) -> Dict[str, Any]:
592 """
593 Delete a project.
595 Args:
596 project_id (str): The ID of the project to delete.
598 Returns:
599 Dict[str, Any]: A dictionary that contains the ID of the deleted project.
600 """
602 # Delete the project.
603 if os.path.isdir(DATA_DIRECTORY / project_id):
604 shutil.rmtree(DATA_DIRECTORY / project_id, ignore_errors=True)
606 # Return the deleted project id.
607 return {
608 "project_id": project_id,
609 "detail": "The deletion of project with id '{project_id_str}' is accepted.".format(
610 project_id_str=str(project_id),
611 ),
612 }
615###
616### ROUTE: Get metadata.
617###
618@app.get(
619 "/api/projects/{project_id}/metadata",
620 tags=["Projects"],
621 status_code=status.HTTP_200_OK,
622)
623async def get_metadata(
624 project_id: str = Path(
625 ...,
626 description="The ID of the project.",
627 ),
628) -> Dict[str, Any]:
629 """
630 Get metadata.
632 Args:
633 project_id (str): The ID of the project.
635 Raises:
636 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
638 Returns:
639 Dict[str, Any]: A dictionary that contains metadata.
640 """
642 # Check project id.
643 if project_id not in (await get_projects()):
644 raise HTTPException(
645 status_code=status.HTTP_404_NOT_FOUND,
646 detail="The project with id '{project_id_str}' doesn't exist.".format(
647 project_id_str=str(project_id),
648 ),
649 )
651 # Load the project metadata.
652 with open(DATA_DIRECTORY / project_id / "metadata.json", "r") as metadata_fileobject:
653 # Return the project metadata.
654 return {
655 "project_id": project_id,
656 "metadata": json.load(metadata_fileobject),
657 }
660###
661### ROUTE: Download a project in a zip archive.
662###
663@app.get(
664 "/api/projects/{project_id}/download",
665 tags=["Projects"],
666 response_class=FileResponse,
667 status_code=status.HTTP_200_OK,
668)
669async def download_project(
670 background_tasks: BackgroundTasks,
671 project_id: str = Path(
672 ...,
673 description="The ID of the project to download.",
674 ),
675) -> FileResponse:
676 """
677 Download a project in a zip archive.
679 Args:
680 background_tasks (BackgroundTasks): A background task to run after the return statement.
681 project_id (str): The ID of the project to download.
683 Raises:
684 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
686 Returns:
687 FileResponse: A zip archive of the project.
688 """
690 # Check project id.
691 if project_id not in (await get_projects()):
692 raise HTTPException(
693 status_code=status.HTTP_404_NOT_FOUND,
694 detail="The project with id '{project_id_str}' doesn't exist.".format(
695 project_id_str=str(project_id),
696 ),
697 )
699 # Define archive name.
700 archive_name: str = "archive-{project_id_str}.zip".format(project_id_str=str(project_id))
701 archive_path: pathlib.Path = DATA_DIRECTORY / project_id / archive_name
703 # Zip the project in an archive.
704 with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive_filewriter:
705 archive_filewriter.write(DATA_DIRECTORY / project_id / "metadata.json", arcname="metadata.json")
706 archive_filewriter.write(DATA_DIRECTORY / project_id / "status.json", arcname="status.json")
707 archive_filewriter.write(DATA_DIRECTORY / project_id / "texts.json", arcname="texts.json")
708 archive_filewriter.write(DATA_DIRECTORY / project_id / "constraints.json", arcname="constraints.json")
709 archive_filewriter.write(DATA_DIRECTORY / project_id / "settings.json", arcname="settings.json")
710 archive_filewriter.write(DATA_DIRECTORY / project_id / "sampling.json", arcname="sampling.json")
711 archive_filewriter.write(DATA_DIRECTORY / project_id / "clustering.json", arcname="clustering.json")
712 archive_filewriter.write(DATA_DIRECTORY / project_id / "modelization.json", arcname="modelization.json")
713 if "vectors_2D.json" in os.listdir(DATA_DIRECTORY / project_id):
714 archive_filewriter.write(DATA_DIRECTORY / project_id / "vectors_2D.json", arcname="vectors_2D.json")
715 if "vectors_3D.json" in os.listdir(DATA_DIRECTORY / project_id):
716 archive_filewriter.write(DATA_DIRECTORY / project_id / "vectors_3D.json", arcname="vectors_3D.json")
718 # Define a backgroundtask to clear archive after downloading.
719 def clear_after_download_project(): # noqa: WPS430 (nested function)
720 """
721 Delete the archive file.
722 """
724 # Delete archive file.
725 if os.path.exists(archive_path): # pragma: no cover
726 os.remove(archive_path)
728 # Add the background task.
729 background_tasks.add_task(
730 func=clear_after_download_project,
731 )
733 # Return the zip archive of the project.
734 return FileResponse(
735 archive_path,
736 media_type="application/x-zip-compressed",
737 filename=archive_name,
738 )
741###
742### ROUTE: Import a project.
743###
744@app.put(
745 "/api/projects",
746 tags=["Projects"],
747 status_code=status.HTTP_201_CREATED,
748)
749async def import_project(
750 background_tasks: BackgroundTasks,
751 project_archive: UploadFile = File(
752 ...,
753 description="A zip archive representing a project. Use format from `download` route.",
754 # TODO: max_size="8MB",
755 ),
756) -> Dict[str, Any]:
757 """
758 Import a project from a zip archive file.
760 Args:
761 background_tasks (BackgroundTasks): A background task to run after the return statement.
762 project_archive (UploadFile, optional): A zip archive representing a project. Use format from `download` route.
764 Raises:
765 HTTPException: Raises `HTTP_400_NOT_FOUND` if archive is invalid.
767 Returns:
768 Dict[str, Any]: A dictionary that contains the ID of the imported project.
769 """
771 # Check archive type.
772 if project_archive.content_type != "application/x-zip-compressed":
773 raise HTTPException(
774 status_code=status.HTTP_400_BAD_REQUEST,
775 detail="The file type '{project_archive_type}' is not supported. Please use '.zip' file.".format(
776 project_archive_type=str(project_archive.content_type),
777 ),
778 )
780 # Temporarly store zip archive.
781 current_timestamp: float = datetime.now().timestamp()
782 new_current_project_id: str = str(int(current_timestamp * 10**6))
783 import_archive_name: str = "import-{new_current_project_id_str}.zip".format(
784 new_current_project_id_str=str(new_current_project_id)
785 )
786 import_archive_path: pathlib.Path = DATA_DIRECTORY / import_archive_name
787 with open(import_archive_path, "wb") as import_archive_fileobject_w:
788 shutil.copyfileobj(project_archive.file, import_archive_fileobject_w)
790 # Define a backgroundtask to clear archive after importation.
791 def clear_after_import_project(): # noqa: WPS430 (nested function)
792 """
793 Delete the archive file.
794 """
796 # Delete archive file.
797 if os.path.exists(import_archive_path): # pragma: no cover
798 os.remove(import_archive_path)
800 # Add the background task.
801 background_tasks.add_task(
802 func=clear_after_import_project,
803 )
805 # Try to open archive file.
806 try:
807 with zipfile.ZipFile(import_archive_path, "r") as import_archive_file:
808 ###
809 ### Check archive content.
810 ###
811 missing_files: List[str] = [
812 needed_file
813 for needed_file in (
814 "metadata.json",
815 "status.json",
816 "texts.json",
817 "constraints.json",
818 "settings.json",
819 "sampling.json",
820 "clustering.json",
821 "modelization.json", # Will be recomputed during modelization step.
822 # "vectors_2D.json", # Will be recomputed during modelization step.
823 # "vectors_3D.json", # Will be recomputed during modelization step.
824 )
825 if needed_file not in import_archive_file.namelist()
826 ]
827 if len(missing_files) != 0: # noqa: WPS507
828 raise ValueError(
829 "The project archive file doesn't contains the following files: {missing_files_str}.".format(
830 missing_files_str=str(missing_files),
831 )
832 )
834 ###
835 ### Check `metadata.json`.
836 ###
837 with import_archive_file.open("metadata.json") as metadata_fileobject_r:
838 metadata: Dict[str, Any] = json.load(metadata_fileobject_r)
839 metadata["project_id"] = new_current_project_id
840 if (
841 "project_name" not in metadata.keys()
842 or not isinstance(metadata["project_name"], str)
843 or "creation_timestamp" not in metadata.keys()
844 or not isinstance(metadata["creation_timestamp"], float)
845 ):
846 raise ValueError("The project archive file has an invalid `metadata.json` file.")
848 ###
849 ### Check `status.json`.
850 ###
851 with import_archive_file.open("status.json") as status_fileobject_r:
852 project_status: Dict[str, Any] = json.load(status_fileobject_r)
854 # Check `status.state`.
855 if "state" not in project_status.keys():
856 raise ValueError("The project archive file has an invalid `status.json` file (see key `state`).")
858 # Force `status.state` - Case of initialization.
859 if (
860 project_status["state"] == ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION # noqa: WPS514
861 or project_status["state"] == ICGUIStates.INITIALIZATION_WITH_PENDING_MODELIZATION
862 or project_status["state"] == ICGUIStates.INITIALIZATION_WITH_WORKING_MODELIZATION
863 or project_status["state"] == ICGUIStates.INITIALIZATION_WITH_ERRORS
864 ):
865 project_status["state"] = ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION
866 # Force `status.state` - Case of sampling.
867 elif (
868 project_status["state"] == ICGUIStates.SAMPLING_TODO # noqa: WPS514
869 or project_status["state"] == ICGUIStates.SAMPLING_PENDING
870 or project_status["state"] == ICGUIStates.SAMPLING_WORKING
871 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITHOUT_MODELIZATION
872 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_PENDING_MODELIZATION
873 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_WORKING_MODELIZATION
874 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_ERRORS
875 ):
876 project_status["state"] = ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITHOUT_MODELIZATION
877 # Force `status.state` - Case of annotation.
878 elif (
879 project_status["state"] == ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION # noqa: WPS514
880 or project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
881 or project_status["state"] == ICGUIStates.ANNOTATION_WITH_PENDING_MODELIZATION_WITHOUT_CONFLICTS
882 or project_status["state"] == ICGUIStates.ANNOTATION_WITH_WORKING_MODELIZATION_WITHOUT_CONFLICTS
883 or project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
884 or project_status["state"] == ICGUIStates.ANNOTATION_WITH_PENDING_MODELIZATION_WITH_CONFLICTS
885 or project_status["state"] == ICGUIStates.ANNOTATION_WITH_WORKING_MODELIZATION_WITH_CONFLICTS
886 or project_status["state"] == ICGUIStates.IMPORT_AT_ANNOTATION_STEP_WITHOUT_MODELIZATION
887 or project_status["state"] == ICGUIStates.IMPORT_AT_ANNOTATION_STEP_WITH_PENDING_MODELIZATION
888 or project_status["state"] == ICGUIStates.IMPORT_AT_ANNOTATION_STEP_WITH_WORKING_MODELIZATION
889 ):
890 project_status["state"] = ICGUIStates.IMPORT_AT_ANNOTATION_STEP_WITHOUT_MODELIZATION
891 # Force `status.state` - Case of clustering.
892 elif (
893 project_status["state"] == ICGUIStates.CLUSTERING_TODO # noqa: WPS514
894 or project_status["state"] == ICGUIStates.CLUSTERING_PENDING
895 or project_status["state"] == ICGUIStates.CLUSTERING_WORKING
896 or project_status["state"] == ICGUIStates.IMPORT_AT_CLUSTERING_STEP_WITHOUT_MODELIZATION
897 or project_status["state"] == ICGUIStates.IMPORT_AT_CLUSTERING_STEP_WITH_PENDING_MODELIZATION
898 or project_status["state"] == ICGUIStates.IMPORT_AT_CLUSTERING_STEP_WITH_WORKING_MODELIZATION
899 or project_status["state"] == ICGUIStates.IMPORT_AT_CLUSTERING_STEP_WITH_ERRORS
900 ):
901 project_status["state"] = ICGUIStates.IMPORT_AT_CLUSTERING_STEP_WITHOUT_MODELIZATION
902 # Force `status.state` - Case of iteration end.
903 elif (
904 project_status["state"] == ICGUIStates.ITERATION_END # noqa: WPS514
905 or project_status["state"] == ICGUIStates.IMPORT_AT_ITERATION_END_WITHOUT_MODELIZATION
906 or project_status["state"] == ICGUIStates.IMPORT_AT_ITERATION_END_WITH_PENDING_MODELIZATION
907 or project_status["state"] == ICGUIStates.IMPORT_AT_ITERATION_END_WITH_WORKING_MODELIZATION
908 or project_status["state"] == ICGUIStates.IMPORT_AT_ITERATION_END_WITH_ERRORS
909 ):
910 project_status["state"] = ICGUIStates.IMPORT_AT_ITERATION_END_WITHOUT_MODELIZATION
911 # Force `state` - Case of unknown state.
912 else:
913 raise ValueError("The project archive file has an invalid `status.json` file (see key `state`).")
915 # Force `status.task`.
916 project_status["task"] = None
918 # TODO: Check `texts.json`.
919 with import_archive_file.open("texts.json") as texts_fileobject_r:
920 texts: Dict[str, Dict[str, Any]] = json.load(texts_fileobject_r)
922 # TODO: Check `constraints.json`.
923 with import_archive_file.open("constraints.json") as constraints_fileobject_r:
924 constraints: Dict[str, Dict[str, Any]] = json.load(constraints_fileobject_r)
926 # TODO: Check `settings.json`.
927 with import_archive_file.open("settings.json") as settings_fileobject_r:
928 settings: Dict[str, Dict[str, Any]] = json.load(settings_fileobject_r)
930 # TODO: Check `sampling.json`.
931 with import_archive_file.open("sampling.json") as sampling_fileobject_r:
932 sampling: Dict[str, List[str]] = json.load(sampling_fileobject_r)
934 # TODO: Check `clustering.json`.
935 with import_archive_file.open("clustering.json") as clustering_fileobject_r:
936 clustering: Dict[str, Dict[str, str]] = json.load(clustering_fileobject_r)
938 # TODO: Check `modelization.json`.
939 with import_archive_file.open("modelization.json") as modelization_fileobject_r:
940 modelization: Dict[str, Dict[str, Any]] = json.load(modelization_fileobject_r)
942 # Error: case of custom raised errors.
943 except ValueError as value_error:
944 raise HTTPException(
945 status_code=status.HTTP_400_BAD_REQUEST,
946 detail=str(value_error),
947 )
949 # Error: other raised errors.
950 except Exception:
951 raise HTTPException(
952 status_code=status.HTTP_400_BAD_REQUEST,
953 detail="An error occurs in project import. Project archive is probably invalid.",
954 )
956 # Create the directory and subdirectories of the new project.
957 os.mkdir(DATA_DIRECTORY / metadata["project_id"])
959 # Store `metadata.json`.
960 with open(DATA_DIRECTORY / metadata["project_id"] / "metadata.json", "w") as metadata_fileobject_w:
961 json.dump(metadata, metadata_fileobject_w, indent=4)
963 # Store `status.json`.
964 with open(DATA_DIRECTORY / metadata["project_id"] / "status.json", "w") as status_fileobject_w:
965 json.dump(project_status, status_fileobject_w, indent=4)
967 # Store `texts.json`.
968 with open(DATA_DIRECTORY / metadata["project_id"] / "texts.json", "w") as texts_fileobject_w:
969 json.dump(texts, texts_fileobject_w, indent=4)
971 # Store `constraints.json`.
972 with open(DATA_DIRECTORY / metadata["project_id"] / "constraints.json", "w") as constraints_fileobject_w:
973 json.dump(constraints, constraints_fileobject_w, indent=4)
975 # Store `settings.json`.
976 with open(DATA_DIRECTORY / metadata["project_id"] / "settings.json", "w") as settings_fileobject_w:
977 json.dump(settings, settings_fileobject_w, indent=4)
979 # Store `sampling.json`.
980 with open(DATA_DIRECTORY / metadata["project_id"] / "sampling.json", "w") as sampling_fileobject_w:
981 json.dump(sampling, sampling_fileobject_w, indent=4)
983 # Store `clustering.json`.
984 with open(DATA_DIRECTORY / metadata["project_id"] / "clustering.json", "w") as clustering_fileobject_w:
985 json.dump(clustering, clustering_fileobject_w, indent=4)
987 # Store `modelization.json`.
988 with open(DATA_DIRECTORY / metadata["project_id"] / "modelization.json", "w") as modelization_fileobject_w:
989 json.dump(modelization, modelization_fileobject_w, indent=4)
991 # Return the new ID of the imported project.
992 return {
993 "project_id": metadata["project_id"],
994 "detail": (
995 "The project with name '{project_name_str}' has been imported. It has the id '{project_id_str}'.".format(
996 project_name_str=str(metadata["project_name"]),
997 project_id_str=str(metadata["project_id"]),
998 )
999 ),
1000 }
1003###
1004### ROUTE: Get HTML project home page.
1005###
1006@app.get(
1007 "/gui/projects/{project_id}",
1008 tags=["Projects"],
1009 response_class=Response,
1010 status_code=status.HTTP_200_OK,
1011)
1012async def get_html_project_home_page(
1013 request: Request,
1014 project_id: str = Path(
1015 ...,
1016 description="The ID of the project.",
1017 ),
1018) -> Response:
1019 """
1020 Get HTML project home page.
1022 Args:
1023 request (Request): The request context.
1024 project_id (str): The ID of the project.
1026 Returns:
1027 Response: The requested page.
1028 """
1030 # Return HTML project home page.
1031 try:
1032 return templates.TemplateResponse(
1033 name="project_home.html",
1034 context={
1035 "request": request,
1036 # Get the project ID.
1037 "project_id": project_id,
1038 # Get the project metadata (ID, name, creation date).
1039 "metadata": (await get_metadata(project_id=project_id))["metadata"],
1040 # Get the project status (iteration, step name and status, modelization state and conflict).
1041 "status": (await get_status(project_id=project_id))["status"],
1042 # Get the project constraints.
1043 "constraints": (
1044 await get_constraints(
1045 project_id=project_id,
1046 without_hidden_constraints=True,
1047 sorted_by=ConstraintsSortOptions.ITERATION_OF_SAMPLING,
1048 sorted_reverse=False,
1049 )
1050 )["constraints"],
1051 },
1052 status_code=status.HTTP_200_OK,
1053 )
1055 # Case of error: Return HTML error page.
1056 except HTTPException as error:
1057 # Return HTML error page.
1058 return templates.TemplateResponse(
1059 name="error.html",
1060 context={
1061 "request": request,
1062 "status_code": error.status_code,
1063 "detail": error.detail,
1064 },
1065 status_code=error.status_code,
1066 )
1069# ==============================================================================
1070# DEFINE ROUTES FOR STATUS
1071# ==============================================================================
1074###
1075### ROUTE: Get status.
1076###
1077@app.get(
1078 "/api/projects/{project_id}/status",
1079 tags=["Status"],
1080 status_code=status.HTTP_200_OK,
1081)
1082async def get_status(
1083 project_id: str = Path(
1084 ...,
1085 description="The ID of the project.",
1086 ),
1087) -> Dict[str, Any]:
1088 """
1089 Get status.
1091 Args:
1092 project_id (str): The ID of the project.
1094 Raises:
1095 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
1097 Returns:
1098 Dict[str, Any]: A dictionary that contains status.
1099 """
1101 # Check project id.
1102 if project_id not in (await get_projects()):
1103 raise HTTPException(
1104 status_code=status.HTTP_404_NOT_FOUND,
1105 detail="The project with id '{project_id_str}' doesn't exist.".format(
1106 project_id_str=str(project_id),
1107 ),
1108 )
1110 # Load status file.
1111 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
1112 project_status: Dict[str, Any] = json.load(status_fileobject)
1113 project_status["state_details"] = get_ICGUIStates_details(state=project_status["state"])
1115 # Return the requested status.
1116 return {"project_id": project_id, "status": project_status}
1119###
1120### ROUTE: Move to next iteration after clustering step.
1121###
1122@app.post(
1123 "/api/projects/{project_id}/iterations",
1124 tags=["Status"],
1125 status_code=status.HTTP_201_CREATED,
1126)
1127async def move_to_next_iteration(
1128 project_id: str = Path(
1129 ...,
1130 description="The ID of the project.",
1131 ),
1132) -> Dict[str, Any]:
1133 """
1134 Move to next iteration after clustering step.
1136 Args:
1137 project_id (str): The ID of the project.
1139 Raises:
1140 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
1141 HTTPException: Raises `HTTP_403_FORBIDDEN` if the project didn't complete its clustering step.
1143 Returns:
1144 Dict[str, Any]: A dictionary that contains the ID of the new iteration.
1145 """
1147 # Check project id.
1148 if project_id not in (await get_projects()):
1149 raise HTTPException(
1150 status_code=status.HTTP_404_NOT_FOUND,
1151 detail="The project with id '{project_id_str}' doesn't exist.".format(
1152 project_id_str=str(project_id),
1153 ),
1154 )
1156 # Lock status file in order to check project status for this step.
1157 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
1158 ###
1159 ### Load needed data.
1160 ###
1162 # Load status file.
1163 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject_r:
1164 project_status: Dict[str, Any] = json.load(status_fileobject_r)
1166 # Load settings file.
1167 with open(DATA_DIRECTORY / project_id / "settings.json", "r") as settings_fileobject_r:
1168 project_settings: Dict[str, Any] = json.load(settings_fileobject_r)
1170 # Get current iteration id.
1171 current_iteration_id: int = project_status["iteration_id"]
1173 ###
1174 ### Check parameters.
1175 ###
1177 # Check project status.
1178 if project_status["state"] != ICGUIStates.ITERATION_END:
1179 raise HTTPException(
1180 status_code=status.HTTP_403_FORBIDDEN,
1181 detail="The project with id '{project_id_str}' hasn't completed its clustering step on iteration '{iteration_id_str}'.".format(
1182 project_id_str=str(project_id),
1183 iteration_id_str=str(current_iteration_id),
1184 ),
1185 )
1187 ###
1188 ### Update data.
1189 ###
1191 # Define new iteration id.
1192 new_iteration_id: int = current_iteration_id + 1
1194 # Initialize status for the new iteration.
1195 project_status["iteration_id"] = new_iteration_id
1196 project_status["state"] = ICGUIStates.SAMPLING_TODO
1198 # Initialize settings for the new iteration.
1199 project_settings[str(new_iteration_id)] = {
1200 "sampling": (
1201 default_SamplingSettingsModel().to_dict()
1202 if (current_iteration_id == 0)
1203 else project_settings[str(current_iteration_id)]["sampling"]
1204 ),
1205 "preprocessing": project_settings[str(current_iteration_id)]["preprocessing"],
1206 "vectorization": project_settings[str(current_iteration_id)]["vectorization"],
1207 "clustering": project_settings[str(current_iteration_id)]["clustering"],
1208 }
1210 ###
1211 ### Store updated data.
1212 ###
1214 # Store project settings.
1215 with open(DATA_DIRECTORY / project_id / "settings.json", "w") as settings_fileobject_w:
1216 json.dump(project_settings, settings_fileobject_w, indent=4)
1218 # Store project status.
1219 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
1220 json.dump(project_status, status_fileobject_w, indent=4)
1222 # Return the new iteration id.
1223 return {
1224 "project_id": project_id,
1225 "iteration_id": new_iteration_id,
1226 "detail": "The project with id '{project_id_str}' is now on iteration with id '{iteration_id_str}'.".format(
1227 project_id_str=str(project_id),
1228 iteration_id_str=str(new_iteration_id),
1229 ),
1230 }
1233# ==============================================================================
1234# DEFINE ROUTES FOR TEXTS
1235# ==============================================================================
1238###
1239### ROUTE: Get texts.
1240###
1241@app.get(
1242 "/api/projects/{project_id}/texts",
1243 tags=["Texts"],
1244 status_code=status.HTTP_200_OK,
1245)
1246async def get_texts(
1247 project_id: str = Path(
1248 ...,
1249 description="The ID of the project.",
1250 ),
1251 without_deleted_texts: bool = Query(
1252 True,
1253 description="The option to not return deleted texts. Defaults to `True`.",
1254 ),
1255 sorted_by: TextsSortOptions = Query(
1256 TextsSortOptions.ALPHABETICAL,
1257 description="The option to sort texts. Defaults to `ALPHABETICAL`.",
1258 ),
1259 sorted_reverse: bool = Query(
1260 False,
1261 description="The option to reverse texts order. Defaults to `False`.",
1262 ),
1263 # TODO: filter_text
1264 # TODO: limit_size + offset
1265) -> Dict[str, Any]:
1266 """
1267 Get texts.
1269 Args:
1270 project_id (str): The ID of the project.
1271 without_deleted_texts (bool): The option to not return deleted texts. Defaults to `True`.
1272 sorted_by (TextsSortOptions, optional): The option to sort texts. Defaults to `ALPHABETICAL`.
1273 sorted_reverse (bool, optional): The option to reverse texts order. Defaults to `False`.
1275 Raises:
1276 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
1278 Returns:
1279 Dict[str, Any]: A dictionary that contains texts.
1280 """
1282 # Check project id.
1283 if project_id not in (await get_projects()):
1284 raise HTTPException(
1285 status_code=status.HTTP_404_NOT_FOUND,
1286 detail="The project with id '{project_id_str}' doesn't exist.".format(
1287 project_id_str=str(project_id),
1288 ),
1289 )
1291 ###
1292 ### Load needed data.
1293 ###
1295 # Load texts.
1296 with open(DATA_DIRECTORY / project_id / "texts.json", "r") as texts_fileobject:
1297 texts: Dict[str, Any] = {
1298 text_id: text_value
1299 for text_id, text_value in json.load(texts_fileobject).items()
1300 if (without_deleted_texts is False or text_value["is_deleted"] is False)
1301 }
1303 ###
1304 ### Sort texts.
1305 ###
1307 # Define the values selection method.
1308 def get_value_for_texts_sorting(text_to_sort: Tuple[str, Dict[str, Any]]) -> Any: # noqa: WPS430 (nested function)
1309 """Return the values expected for texts sorting.
1311 Args:
1312 text_to_sort (Tuple[str, Dict[str, Any]]): A text (from `.items()`).
1314 Returns:
1315 Any: The expected values of the text need for the sort.
1316 """
1317 # By text id.
1318 if sorted_by == TextsSortOptions.ID:
1319 return text_to_sort[0]
1320 # By text value.
1321 if sorted_by == TextsSortOptions.ALPHABETICAL:
1322 return text_to_sort[1]["text_preprocessed"]
1323 # By deletion status.
1324 #### if sorted_by == TextsSortOptions.IS_DELETED:
1325 return text_to_sort[1]["is_deleted"]
1327 # Sorted the texts to return.
1328 sorted_texts: Dict[str, Any] = dict(
1329 sorted(
1330 texts.items(),
1331 key=get_value_for_texts_sorting,
1332 reverse=sorted_reverse,
1333 )
1334 )
1336 # Return the requested texts.
1337 return {
1338 "project_id": project_id,
1339 "texts": sorted_texts,
1340 # Get the request parameters.
1341 "parameters": {
1342 "without_deleted_texts": without_deleted_texts,
1343 "sorted_by": sorted_by.value,
1344 "sorted_reverse": sorted_reverse,
1345 },
1346 }
1349###
1350### ROUTE: Delete a text.
1351###
1352@app.put(
1353 "/api/projects/{project_id}/texts/{text_id}/delete",
1354 tags=["Texts"],
1355 status_code=status.HTTP_202_ACCEPTED,
1356)
1357async def delete_text(
1358 project_id: str = Path(
1359 ...,
1360 description="The ID of the project.",
1361 ),
1362 text_id: str = Path(
1363 ...,
1364 description="The ID of the text.",
1365 ),
1366) -> Dict[str, Any]:
1367 """
1368 Delete a text.
1370 Args:
1371 project_id (str): The ID of the project.
1372 text_id (str): The ID of the text.
1374 Raises:
1375 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
1376 HTTPException: Raises `HTTP_404_NOT_FOUND` if the text with id `text_id` to delete doesn't exist.
1377 HTTPException: Raises `HTTP_403_FORBIDDEN` if the current status of the project doesn't allow modification.
1379 Returns:
1380 Dict[str, Any]: A dictionary that contains the ID of deleted text.
1381 """
1383 # Check project id.
1384 if project_id not in (await get_projects()):
1385 raise HTTPException(
1386 status_code=status.HTTP_404_NOT_FOUND,
1387 detail="The project with id '{project_id_str}' doesn't exist.".format(
1388 project_id_str=str(project_id),
1389 ),
1390 )
1392 # Lock status file in order to check project status for this step.
1393 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
1394 ###
1395 ### Load needed data.
1396 ###
1398 # Load status file.
1399 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
1400 project_status: Dict[str, Any] = json.load(status_fileobject)
1402 # Load texts file.
1403 with open(DATA_DIRECTORY / project_id / "texts.json", "r") as texts_fileobject_r:
1404 texts: Dict[str, Any] = json.load(texts_fileobject_r)
1406 # Load constraints file.
1407 with open(DATA_DIRECTORY / project_id / "constraints.json", "r") as constraints_fileobject_r:
1408 constraints: Dict[str, Any] = json.load(constraints_fileobject_r)
1410 ###
1411 ### Check parameters.
1412 ###
1414 # Check text id.
1415 if text_id not in texts.keys():
1416 raise HTTPException(
1417 status_code=status.HTTP_404_NOT_FOUND,
1418 detail="In project with id '{project_id_str}', the text with id '{text_id_str}' to delete doesn't exist.".format(
1419 project_id_str=str(project_id),
1420 text_id_str=str(text_id),
1421 ),
1422 )
1424 # Check status.
1425 if (
1426 project_status["state"] != ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION # noqa: WPS514
1427 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
1428 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
1429 ):
1430 raise HTTPException(
1431 status_code=status.HTTP_403_FORBIDDEN,
1432 detail="The project with id '{project_id_str}' doesn't allow modification during this state (state='{state_str}').".format(
1433 project_id_str=str(project_id),
1434 state_str=str(project_status["state"]),
1435 ),
1436 )
1438 ###
1439 ### Update data.
1440 ###
1442 # Update status by forcing "outdated" status.
1443 if project_status["state"] == ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION:
1444 project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
1445 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS:
1446 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
1447 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS:
1448 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
1450 # Update texts by deleting the text.
1451 texts[text_id]["is_deleted"] = True
1453 # Update constraints by hidding those associated with the deleted text.
1454 for constraint_id, constraint_value in constraints.items():
1455 data_id1: str = constraint_value["data"]["id_1"]
1456 data_id2: str = constraint_value["data"]["id_2"]
1458 if text_id in {
1459 data_id1,
1460 data_id2,
1461 }:
1462 constraints[constraint_id]["is_hidden"] = True
1464 ###
1465 ### Store updated data.
1466 ###
1468 # Store updated status in file.
1469 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
1470 json.dump(project_status, status_fileobject_w, indent=4)
1472 # Store updated texts in file.
1473 with open(DATA_DIRECTORY / project_id / "texts.json", "w") as texts_fileobject_w:
1474 json.dump(texts, texts_fileobject_w, indent=4)
1476 # Store updated constraints in file.
1477 with open(DATA_DIRECTORY / project_id / "constraints.json", "w") as constraints_fileobject_w:
1478 json.dump(constraints, constraints_fileobject_w, indent=4)
1480 # Return statement.
1481 return {
1482 "project_id": project_id,
1483 "text_id": text_id,
1484 "detail": "In project with id '{project_id_str}', the text with id '{text_id_str}' has been deleted. Several constraints have been hidden.".format(
1485 project_id_str=str(project_id),
1486 text_id_str=str(text_id),
1487 ),
1488 }
1491###
1492### ROUTE: Undelete a text.
1493###
1494@app.put(
1495 "/api/projects/{project_id}/texts/{text_id}/undelete",
1496 tags=["Texts"],
1497 status_code=status.HTTP_202_ACCEPTED,
1498)
1499async def undelete_text(
1500 project_id: str = Path(
1501 ...,
1502 description="The ID of the project.",
1503 ),
1504 text_id: str = Path(
1505 ...,
1506 description="The ID of the text.",
1507 ),
1508) -> Dict[str, Any]:
1509 """
1510 Undelete a text.
1512 Args:
1513 project_id (str): The ID of the project.
1514 text_id (str): The ID of the text.
1516 Raises:
1517 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
1518 HTTPException: Raises `HTTP_404_NOT_FOUND` if the text with id `text_id` to undelete doesn't exist.
1519 HTTPException: Raises `HTTP_403_FORBIDDEN` if the current status of the project doesn't allow modification.
1521 Returns:
1522 Dict[str, Any]: A dictionary that contains the ID of undeleted text.
1523 """
1525 # Check project id.
1526 if project_id not in (await get_projects()):
1527 raise HTTPException(
1528 status_code=status.HTTP_404_NOT_FOUND,
1529 detail="The project with id '{project_id_str}' doesn't exist.".format(
1530 project_id_str=str(project_id),
1531 ),
1532 )
1534 # Lock status file in order to check project status for this step.
1535 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
1536 ###
1537 ### Load needed data.
1538 ###
1540 # Load status file.
1541 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
1542 project_status: Dict[str, Any] = json.load(status_fileobject)
1544 # Load texts file.
1545 with open(DATA_DIRECTORY / project_id / "texts.json", "r") as texts_fileobject_r:
1546 texts: Dict[str, Any] = json.load(texts_fileobject_r)
1548 # Load constraints file.
1549 with open(DATA_DIRECTORY / project_id / "constraints.json", "r") as constraints_fileobject_r:
1550 constraints: Dict[str, Any] = json.load(constraints_fileobject_r)
1552 ###
1553 ### Check parameters.
1554 ###
1556 # Check text id.
1557 if text_id not in texts.keys():
1558 raise HTTPException(
1559 status_code=status.HTTP_404_NOT_FOUND,
1560 detail="In project with id '{project_id_str}', the text with id '{text_id_str}' to undelete doesn't exist.".format(
1561 project_id_str=str(project_id),
1562 text_id_str=str(text_id),
1563 ),
1564 )
1566 # Check status.
1567 if (
1568 project_status["state"] != ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION # noqa: WPS514
1569 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
1570 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
1571 ):
1572 raise HTTPException(
1573 status_code=status.HTTP_403_FORBIDDEN,
1574 detail="The project with id '{project_id_str}' doesn't allow modification during this state (state='{state_str}').".format(
1575 project_id_str=str(project_id),
1576 state_str=str(project_status["state"]),
1577 ),
1578 )
1580 ###
1581 ### Update data.
1582 ###
1584 # Update status by forcing "outdated" status.
1585 if project_status["state"] == ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION:
1586 project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
1587 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS:
1588 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
1589 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS:
1590 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
1592 # Update texts by undeleting the text.
1593 texts[text_id]["is_deleted"] = False
1595 # Update constraints by unhidding those associated with the undeleted text.
1596 for constraint_id, constraint_value in constraints.items():
1597 data_id1: str = constraint_value["data"]["id_1"]
1598 data_id2: str = constraint_value["data"]["id_2"]
1600 if text_id in {data_id1, data_id2}:
1601 constraints[constraint_id]["is_hidden"] = (
1602 texts[data_id1]["is_deleted"] is True or texts[data_id2]["is_deleted"] is True
1603 )
1605 ###
1606 ### Store updated data.
1607 ###
1609 # Store updated status in file.
1610 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
1611 json.dump(project_status, status_fileobject_w, indent=4)
1613 # Store updated texts in file.
1614 with open(DATA_DIRECTORY / project_id / "texts.json", "w") as texts_fileobject_w:
1615 json.dump(texts, texts_fileobject_w, indent=4)
1617 # Store updated constraints in file.
1618 with open(DATA_DIRECTORY / project_id / "constraints.json", "w") as constraints_fileobject_w:
1619 json.dump(constraints, constraints_fileobject_w, indent=4)
1621 # Return statement.
1622 return {
1623 "project_id": project_id,
1624 "text_id": text_id,
1625 "detail": "In project with id '{project_id_str}', the text with id '{text_id_str}' has been undeleted. Several constraints have been unhidden.".format(
1626 project_id_str=str(project_id),
1627 text_id_str=str(text_id),
1628 ),
1629 }
1632###
1633### ROUTE: Rename a text.
1634###
1635@app.put(
1636 "/api/projects/{project_id}/texts/{text_id}/rename",
1637 tags=["Texts"],
1638 status_code=status.HTTP_202_ACCEPTED,
1639)
1640async def rename_text(
1641 project_id: str = Path(
1642 ...,
1643 description="The ID of the project.",
1644 ),
1645 text_id: str = Path(
1646 ...,
1647 description="The ID of the text.",
1648 ),
1649 text_value: str = Query(
1650 ...,
1651 description="The new value of the text.",
1652 min_length=3,
1653 max_length=256,
1654 ),
1655) -> Dict[str, Any]:
1656 """
1657 Rename a text.
1659 Args:
1660 project_id (str): The ID of the project.
1661 text_id (str): The ID of the text.
1662 text_value (str): The new value of the text.
1664 Raises:
1665 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
1666 HTTPException: Raises `HTTP_404_NOT_FOUND` if the text with id `text_id` to rename doesn't exist.
1667 HTTPException: Raises `HTTP_403_FORBIDDEN` if the current status of the project doesn't allow modification.
1669 Returns:
1670 Dict[str, Any]: A dictionary that contains the ID of renamed text.
1671 """
1673 # Check project id.
1674 if project_id not in (await get_projects()):
1675 raise HTTPException(
1676 status_code=status.HTTP_404_NOT_FOUND,
1677 detail="The project with id '{project_id_str}' doesn't exist.".format(
1678 project_id_str=str(project_id),
1679 ),
1680 )
1682 # Lock status file in order to check project status for this step.
1683 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
1684 ###
1685 ### Load needed data.
1686 ###
1688 # Load status file.
1689 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
1690 project_status: Dict[str, Any] = json.load(status_fileobject)
1692 # Load texts file.
1693 with open(DATA_DIRECTORY / project_id / "texts.json", "r") as texts_fileobject_r:
1694 texts: Dict[str, Any] = json.load(texts_fileobject_r)
1696 ###
1697 ### Check parameters.
1698 ###
1700 # Check text id.
1701 if text_id not in texts.keys():
1702 raise HTTPException(
1703 status_code=status.HTTP_404_NOT_FOUND,
1704 detail="In project with id '{project_id_str}', the text with id '{text_id_str}' to rename doesn't exist.".format(
1705 project_id_str=str(project_id),
1706 text_id_str=str(text_id),
1707 ),
1708 )
1710 # Check status.
1711 if (
1712 project_status["state"] != ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION # noqa: WPS514
1713 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
1714 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
1715 ):
1716 raise HTTPException(
1717 status_code=status.HTTP_403_FORBIDDEN,
1718 detail="The project with id '{project_id_str}' doesn't allow modification during this state (state='{state_str}').".format(
1719 project_id_str=str(project_id),
1720 state_str=str(project_status["state"]),
1721 ),
1722 )
1724 ###
1725 ### Update data.
1726 ###
1728 # Update status by forcing "outdated" status.
1729 if project_status["state"] == ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION:
1730 project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
1731 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS:
1732 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
1733 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS:
1734 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
1736 # Update texts by renaming the new text.
1737 texts[text_id]["text"] = text_value
1739 ###
1740 ### Store updated data.
1741 ###
1743 # Store updated status in file.
1744 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
1745 json.dump(project_status, status_fileobject_w, indent=4)
1747 # Store updated texts in file.
1748 with open(DATA_DIRECTORY / project_id / "texts.json", "w") as texts_fileobject_w:
1749 json.dump(texts, texts_fileobject_w, indent=4)
1751 # Return statement.
1752 return {
1753 "project_id": project_id,
1754 "text_id": text_id,
1755 "text_value": text_value,
1756 "detail": "In project with id '{project_id_str}', the text with id '{text_id_str}' has been renamed.".format(
1757 project_id_str=str(project_id),
1758 text_id_str=str(text_id),
1759 ),
1760 }
1763###
1764### ROUTE: Get HTML texts page.
1765###
1766@app.get(
1767 "/gui/projects/{project_id}/texts",
1768 tags=["Texts"],
1769 response_class=Response,
1770 status_code=status.HTTP_200_OK,
1771)
1772async def get_html_texts_page(
1773 request: Request,
1774 project_id: str = Path(
1775 ...,
1776 description="The ID of the project.",
1777 ),
1778 sorted_by: TextsSortOptions = Query(
1779 TextsSortOptions.ALPHABETICAL,
1780 description="The option to sort texts. Defaults to `ALPHABETICAL`.",
1781 ),
1782 sorted_reverse: bool = Query(
1783 False,
1784 description="The option to reverse texts order. Defaults to `False`.",
1785 ),
1786 # TODO: filter_text
1787 # TODO: limit_size + offset
1788) -> Response:
1789 """
1790 Get HTML texts page.
1792 Args:
1793 request (Request): The request context.
1794 project_id (str): The ID of the project.
1795 sorted_by (TextsSortOptions, optional): The option to sort texts. Defaults to `ALPHABETICAL`.
1796 sorted_reverse (bool, optional): The option to reverse texts order. Defaults to `False`.
1798 Returns:
1799 Response: The requested page.
1800 """
1802 # Return HTML constraints page.
1803 try:
1804 return templates.TemplateResponse(
1805 name="texts.html",
1806 context={
1807 "request": request,
1808 # Get the project ID.
1809 "project_id": project_id,
1810 # Get the request parameters.
1811 "parameters": {
1812 "without_deleted_texts": True,
1813 "sorted_by": sorted_by.value,
1814 "sorted_reverse": sorted_reverse,
1815 },
1816 # Get the project metadata (ID, name, creation date).
1817 "metadata": (await get_metadata(project_id=project_id))["metadata"],
1818 # Get the project status (iteration, step name and status, modelization state and conflict).
1819 "status": (await get_status(project_id=project_id))["status"],
1820 # Get the project texts.
1821 "texts": (
1822 await get_texts(
1823 project_id=project_id,
1824 without_deleted_texts=False,
1825 sorted_by=sorted_by,
1826 sorted_reverse=sorted_reverse,
1827 )
1828 )["texts"],
1829 # Get the project constraints.
1830 "constraints": (
1831 await get_constraints(
1832 project_id=project_id,
1833 without_hidden_constraints=True,
1834 sorted_by=ConstraintsSortOptions.ID,
1835 sorted_reverse=False,
1836 )
1837 )["constraints"],
1838 },
1839 status_code=status.HTTP_200_OK,
1840 )
1842 # Case of error: Return HTML error page.
1843 except HTTPException as error:
1844 # Return HTML error page.
1845 return templates.TemplateResponse(
1846 name="error.html",
1847 context={
1848 "request": request,
1849 "status_code": error.status_code,
1850 "detail": error.detail,
1851 },
1852 status_code=error.status_code,
1853 )
1856# ==============================================================================
1857# DEFINE ROUTES FOR CONSTRAINTS
1858# ==============================================================================
1861###
1862### ROUTE: Get constraints.
1863###
1864@app.get(
1865 "/api/projects/{project_id}/constraints",
1866 tags=["Constraints"],
1867 status_code=status.HTTP_200_OK,
1868)
1869async def get_constraints(
1870 project_id: str = Path(
1871 ...,
1872 description="The ID of the project.",
1873 ),
1874 without_hidden_constraints: bool = Query(
1875 True,
1876 description="The option to not return hidden constraints. Defaults to `True`.",
1877 ),
1878 sorted_by: ConstraintsSortOptions = Query(
1879 ConstraintsSortOptions.ID,
1880 description="The option to sort constraints. Defaults to `ID`.",
1881 ),
1882 sorted_reverse: bool = Query(
1883 False,
1884 description="The option to reverse constraints order. Defaults to `False`.",
1885 ),
1886 # TODO: filter_text
1887 # TODO: limit_size + offset
1888) -> Dict[str, Any]:
1889 """
1890 Get constraints.
1892 Args:
1893 project_id (str): The ID of the project.
1894 without_hidden_constraints (bool, optional): The option to not return hidden constraints. Defaults to `True`.
1895 sorted_by (ConstraintsSortOptions, optional): The option to sort constraints. Defaults to `ID`.
1896 sorted_reverse (bool, optional): The option to reverse constraints order. Defaults to `False`.
1898 Raises:
1899 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
1901 Returns:
1902 Dict[str, Any]: A dictionary that contains constraints.
1903 """
1905 # Check project id.
1906 if project_id not in (await get_projects()):
1907 raise HTTPException(
1908 status_code=status.HTTP_404_NOT_FOUND,
1909 detail="The project with id '{project_id_str}' doesn't exist.".format(
1910 project_id_str=str(project_id),
1911 ),
1912 )
1914 ###
1915 ### Load needed data.
1916 ###
1918 # Load constraints.
1919 with open(DATA_DIRECTORY / project_id / "constraints.json", "r") as constraints_fileobject:
1920 constraints: Dict[str, Any] = {
1921 constraint_id: constraint_value
1922 for constraint_id, constraint_value in json.load(constraints_fileobject).items()
1923 if (without_hidden_constraints is False or constraint_value["is_hidden"] is False)
1924 }
1926 # Load texts.
1927 with open(DATA_DIRECTORY / project_id / "texts.json", "r") as texts_fileobject:
1928 texts: Dict[str, Any] = json.load(texts_fileobject)
1930 ###
1931 ### Sort constraints.
1932 ###
1934 # Define the values selection method.
1935 def get_value_for_constraints_sorting( # noqa: WPS430 (nested function)
1936 constraint_to_sort: Tuple[str, Dict[str, Any]]
1937 ) -> Any:
1938 """Return the values expected for constraints sorting.
1940 Args:
1941 constraint_to_sort (Tuple[str, Dict[str, Any]]): A constraint (from `.items()`).
1943 Returns:
1944 Any: The expected values of the constraint need for the sort.
1945 """
1946 # By constraint id.
1947 if sorted_by == ConstraintsSortOptions.ID:
1948 return constraint_to_sort[0]
1949 # By texts.
1950 if sorted_by == ConstraintsSortOptions.TEXT:
1951 return (
1952 texts[constraint_to_sort[1]["data"]["id_1"]]["text"],
1953 texts[constraint_to_sort[1]["data"]["id_2"]]["text"],
1954 )
1955 # By constraint type.
1956 if sorted_by == ConstraintsSortOptions.CONSTRAINT_TYPE:
1957 return (
1958 constraint_to_sort[1]["constraint_type"] is None,
1959 constraint_to_sort[1]["constraint_type"] == "CANNOT_LINK",
1960 constraint_to_sort[1]["constraint_type"] == "MUST_LINK",
1961 )
1962 # By date of update.
1963 if sorted_by == ConstraintsSortOptions.DATE_OF_UPDATE:
1964 return constraint_to_sort[1]["date_of_update"] if constraint_to_sort[1]["date_of_update"] is not None else 0
1965 # By iteration of sampling.
1966 if sorted_by == ConstraintsSortOptions.ITERATION_OF_SAMPLING:
1967 return constraint_to_sort[1]["iteration_of_sampling"]
1968 # To annotation.
1969 if sorted_by == ConstraintsSortOptions.TO_ANNOTATE:
1970 return constraint_to_sort[1]["to_annotate"] is False
1971 # To review.
1972 if sorted_by == ConstraintsSortOptions.TO_REVIEW:
1973 return constraint_to_sort[1]["to_review"] is False
1974 # To fix conflict.
1975 #### if sorted_by == ConstraintsSortOptions.TO_FIX_CONFLICT:
1976 return constraint_to_sort[1]["to_fix_conflict"] is False
1978 # Sorted the constraints to return.
1979 sorted_constraints: Dict[str, Any] = dict(
1980 sorted(
1981 constraints.items(),
1982 key=get_value_for_constraints_sorting,
1983 reverse=sorted_reverse,
1984 )
1985 )
1987 # Return the requested constraints.
1988 return {
1989 "project_id": project_id,
1990 "constraints": sorted_constraints,
1991 # Get the request parameters.
1992 "parameters": {
1993 "without_hidden_constraints": without_hidden_constraints,
1994 "sorted_by": sorted_by.value,
1995 "sorted_reverse": sorted_reverse,
1996 },
1997 }
2000###
2001### ROUTE: Annotate a constraint.
2002###
2003@app.put(
2004 "/api/projects/{project_id}/constraints/{constraint_id}/annotate",
2005 tags=["Constraints"],
2006 status_code=status.HTTP_202_ACCEPTED,
2007)
2008async def annotate_constraint(
2009 project_id: str = Path(
2010 ...,
2011 description="The ID of the project.",
2012 ),
2013 constraint_id: str = Path(
2014 ...,
2015 description="The ID of the constraint.",
2016 ),
2017 constraint_type: Optional[ConstraintsValues] = Query(
2018 None,
2019 description="The type of constraint to annotate. Defaults to `None`.",
2020 ),
2021) -> Dict[str, Any]:
2022 """
2023 Annotate a constraint.
2025 Args:
2026 project_id (str): The ID of the project.
2027 constraint_id (str): The ID of the constraint.
2028 constraint_type (Optional[ConstraintsValues]): The type of constraint to annotate. Defaults to `None`.
2030 Raises:
2031 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
2032 HTTPException: Raises `HTTP_404_NOT_FOUND` if the constraint with id `constraint_id` to annotate doesn't exist.
2033 HTTPException: Raises `HTTP_403_FORBIDDEN` if the current status of the project doesn't allow modification.
2035 Returns:
2036 Dict[str, Any]: A dictionary that contains the ID of annotated constraint.
2037 """
2039 # Check project id.
2040 if project_id not in (await get_projects()):
2041 raise HTTPException(
2042 status_code=status.HTTP_404_NOT_FOUND,
2043 detail="The project with id '{project_id_str}' doesn't exist.".format(
2044 project_id_str=str(project_id),
2045 ),
2046 )
2048 # Lock status file in order to check project status for this step.
2049 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
2050 ###
2051 ### Load needed data.
2052 ###
2054 # Load status file.
2055 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
2056 project_status: Dict[str, Any] = json.load(status_fileobject)
2058 # Load constraints file.
2059 with open(DATA_DIRECTORY / project_id / "constraints.json", "r") as constraints_fileobject_r:
2060 constraints: Dict[str, Any] = json.load(constraints_fileobject_r)
2062 ###
2063 ### Check parameters.
2064 ###
2066 # Check constraint id.
2067 if constraint_id not in constraints.keys():
2068 raise HTTPException(
2069 status_code=status.HTTP_404_NOT_FOUND,
2070 detail="In project with id '{project_id_str}', the constraint with id '{constraint_id_str}' to annotate doesn't exist.".format(
2071 project_id_str=str(project_id),
2072 constraint_id_str=str(constraint_id),
2073 ),
2074 )
2076 # Check status.
2077 if (
2078 project_status["state"] != ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION # noqa: WPS514
2079 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2080 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
2081 ):
2082 raise HTTPException(
2083 status_code=status.HTTP_403_FORBIDDEN,
2084 detail="The project with id '{project_id_str}' doesn't allow modification during this state (state='{state_str}').".format(
2085 project_id_str=str(project_id),
2086 state_str=str(project_status["state"]),
2087 ),
2088 )
2090 ###
2091 ### Update data.
2092 ###
2094 # Update status by forcing "outdated" status.
2095 if project_status["state"] == ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION:
2096 project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2097 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS:
2098 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2099 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS:
2100 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
2102 # Update constraints by updating the constraint history.
2103 constraints[constraint_id]["constraint_type_previous"].append(constraints[constraint_id]["constraint_type"])
2105 # Update constraints by annotating the constraint.
2106 constraints[constraint_id]["constraint_type"] = constraint_type
2107 constraints[constraint_id]["date_of_update"] = datetime.now().timestamp()
2109 # Force annotation status.
2110 constraints[constraint_id]["to_annotate"] = False
2112 ###
2113 ### Store updated data.
2114 ###
2116 # Store updated status in file.
2117 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
2118 json.dump(project_status, status_fileobject_w, indent=4)
2120 # Store updated constraints in file.
2121 with open(DATA_DIRECTORY / project_id / "constraints.json", "w") as constraints_fileobject_w:
2122 json.dump(constraints, constraints_fileobject_w, indent=4)
2124 # Return statement.
2125 return {
2126 "project_id": project_id,
2127 "constraint_id": constraint_id,
2128 "detail": "In project with id '{project_id_str}', the constraint with id '{constraint_id_str}' has been annotated at `{constraint_type_str}`.".format(
2129 project_id_str=str(project_id),
2130 constraint_id_str=str(constraint_id),
2131 constraint_type_str="None" if (constraint_type is None) else str(constraint_type.value),
2132 ),
2133 }
2136###
2137### ROUTE: Review a constraint.
2138###
2139@app.put(
2140 "/api/projects/{project_id}/constraints/{constraint_id}/review",
2141 tags=["Constraints"],
2142 status_code=status.HTTP_202_ACCEPTED,
2143)
2144async def review_constraint(
2145 project_id: str = Path(
2146 ...,
2147 description="The ID of the project.",
2148 ),
2149 constraint_id: str = Path(
2150 ...,
2151 description="The ID of the constraint.",
2152 ),
2153 to_review: bool = Query(
2154 True,
2155 description="The choice to review or not the constraint. Defaults to `True`.",
2156 ),
2157) -> Dict[str, Any]:
2158 """
2159 Review a constraint.
2161 Args:
2162 project_id (str): The ID of the project.
2163 constraint_id (str): The ID of the constraint.
2164 to_review (str): The choice to review or not the constraint. Defaults to `True`.
2166 Raises:
2167 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
2168 HTTPException: Raises `HTTP_404_NOT_FOUND` if the constraint with id `constraint_id` to annotate doesn't exist.
2170 Returns:
2171 Dict[str, Any]: A dictionary that contains the ID of reviewed constraint.
2172 """
2174 # Check project id.
2175 if project_id not in (await get_projects()):
2176 raise HTTPException(
2177 status_code=status.HTTP_404_NOT_FOUND,
2178 detail="The project with id '{project_id_str}' doesn't exist.".format(
2179 project_id_str=str(project_id),
2180 ),
2181 )
2183 # Lock status file in order to check project status for this step.
2184 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
2185 ###
2186 ### Load needed data.
2187 ###
2189 # Load constraints file.
2190 with open(DATA_DIRECTORY / project_id / "constraints.json", "r") as constraints_fileobject_r:
2191 constraints: Dict[str, Any] = json.load(constraints_fileobject_r)
2193 ###
2194 ### Check parameters.
2195 ###
2197 # Check constraint id.
2198 if constraint_id not in constraints.keys():
2199 raise HTTPException(
2200 status_code=status.HTTP_404_NOT_FOUND,
2201 detail="In project with id '{project_id_str}', the constraint with id '{constraint_id_str}' to annotate doesn't exist.".format(
2202 project_id_str=str(project_id),
2203 constraint_id_str=str(constraint_id),
2204 ),
2205 )
2207 ###
2208 ### Update data.
2209 ###
2211 # Update constraints by reviewing the constraint.
2212 constraints[constraint_id]["to_review"] = to_review
2214 ###
2215 ### Store updated data.
2216 ###
2218 # Store updated constraints in file.
2219 with open(DATA_DIRECTORY / project_id / "constraints.json", "w") as constraints_fileobject_w:
2220 json.dump(constraints, constraints_fileobject_w, indent=4)
2222 # Return statement.
2223 return {
2224 "project_id": project_id,
2225 "constraint_id": constraint_id,
2226 "detail": "In project with id '{project_id_str}', the constraint with id '{constraint_id_str}' {review_conclusion}.".format(
2227 project_id_str=str(project_id),
2228 constraint_id_str=str(constraint_id),
2229 review_conclusion="need a review" if (to_review) else "has been reviewed",
2230 ),
2231 }
2234###
2235### ROUTE: Comment a constraint.
2236###
2237@app.put(
2238 "/api/projects/{project_id}/constraints/{constraint_id}/comment",
2239 tags=["Constraints"],
2240 status_code=status.HTTP_202_ACCEPTED,
2241)
2242async def comment_constraint(
2243 project_id: str = Path(
2244 ...,
2245 description="The ID of the project.",
2246 ),
2247 constraint_id: str = Path(
2248 ...,
2249 description="The ID of the constraint.",
2250 ),
2251 constraint_comment: str = Query(
2252 ...,
2253 description="The comment of constraint.",
2254 # min_length=0,
2255 max_length=256,
2256 ),
2257) -> Dict[str, Any]:
2258 """
2259 Comment a constraint.
2261 Args:
2262 project_id (str): The ID of the project.
2263 constraint_id (str): The ID of the constraint.
2264 constraint_comment (str): The comment of constraint.
2266 Raises:
2267 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
2268 HTTPException: Raises `HTTP_404_NOT_FOUND` if the constraint with id `constraint_id` to annotate doesn't exist.
2270 Returns:
2271 Dict[str, Any]: A dictionary that contains the ID of commented constraint.
2272 """
2274 # Check project id.
2275 if project_id not in (await get_projects()):
2276 raise HTTPException(
2277 status_code=status.HTTP_404_NOT_FOUND,
2278 detail="The project with id '{project_id_str}' doesn't exist.".format(
2279 project_id_str=str(project_id),
2280 ),
2281 )
2283 # Lock status file in order to check project status for this step.
2284 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
2285 ###
2286 ### Load needed data.
2287 ###
2289 # Load constraints file.
2290 with open(DATA_DIRECTORY / project_id / "constraints.json", "r") as constraints_fileobject_r:
2291 constraints: Dict[str, Any] = json.load(constraints_fileobject_r)
2293 ###
2294 ### Check parameters.
2295 ###
2297 # Check constraint id.
2298 if constraint_id not in constraints.keys():
2299 raise HTTPException(
2300 status_code=status.HTTP_404_NOT_FOUND,
2301 detail="In project with id '{project_id_str}', the constraint with id '{constraint_id_str}' to annotate doesn't exist.".format(
2302 project_id_str=str(project_id),
2303 constraint_id_str=str(constraint_id),
2304 ),
2305 )
2307 ###
2308 ### Update data.
2309 ###
2311 # Update constraints by commenting the constraint.
2312 constraints[constraint_id]["comment"] = constraint_comment
2314 ###
2315 ### Store updated data.
2316 ###
2318 # Store updated constraints in file.
2319 with open(DATA_DIRECTORY / project_id / "constraints.json", "w") as constraints_fileobject_w:
2320 json.dump(constraints, constraints_fileobject_w, indent=4)
2322 # Return statement.
2323 return {
2324 "project_id": project_id,
2325 "constraint_id": constraint_id,
2326 "constraint_comment": constraint_comment,
2327 "detail": "In project with id '{project_id_str}', the constraint with id '{constraint_id_str}' has been commented.".format(
2328 project_id_str=str(project_id),
2329 constraint_id_str=str(constraint_id),
2330 ),
2331 }
2334###
2335### ROUTE: Approve all constraints.
2336###
2337@app.post(
2338 "/api/projects/{project_id}/constraints/approve",
2339 tags=["Constraints"],
2340 status_code=status.HTTP_201_CREATED,
2341)
2342async def approve_all_constraints(
2343 project_id: str = Path(
2344 ...,
2345 description="The ID of the project.",
2346 ),
2347) -> Dict[str, Any]:
2348 """
2349 Approve all constraints.
2351 Args:
2352 project_id (str): The ID of the project.
2354 Raises:
2355 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
2356 HTTPException: Raises `HTTP_403_FORBIDDEN` if the current status of the project doesn't allow constraints approbation.
2358 Returns:
2359 Dict[str, Any]: A dictionary that contains the confirmation of constraints approbation.
2360 """
2362 # Check project id.
2363 if project_id not in (await get_projects()):
2364 raise HTTPException(
2365 status_code=status.HTTP_404_NOT_FOUND,
2366 detail="The project with id '{project_id_str}' doesn't exist.".format(
2367 project_id_str=str(project_id),
2368 ),
2369 )
2371 # Lock status file in order to check project status for this step.
2372 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
2373 # Load status file.
2374 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
2375 project_status: Dict[str, Any] = json.load(status_fileobject)
2377 # Check status.
2378 if project_status["state"] != ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION:
2379 raise HTTPException(
2380 status_code=status.HTTP_403_FORBIDDEN,
2381 detail="The project with id '{project_id_str}' doesn't allow constraints approbation during this state (state='{state_str}').".format(
2382 project_id_str=str(project_id),
2383 state_str=str(project_status["state"]),
2384 ),
2385 )
2387 ###
2388 ### Update data.
2389 ###
2391 # Update status to clustering step.
2392 project_status["state"] = ICGUIStates.CLUSTERING_TODO
2394 ###
2395 ### Store updated data.
2396 ###
2398 # Store updated status in file.
2399 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
2400 json.dump(project_status, status_fileobject_w, indent=4)
2402 # Return statement.
2403 return {
2404 "project_id": project_id,
2405 "detail": "In project with id '{project_id_str}', the constraints have been approved.".format(
2406 project_id_str=str(project_id),
2407 ),
2408 }
2411###
2412### ROUTE: Get HTML constraints page.
2413###
2414@app.get(
2415 "/gui/projects/{project_id}/constraints",
2416 tags=["Constraints"],
2417 response_class=Response,
2418 status_code=status.HTTP_200_OK,
2419)
2420async def get_html_constraints_page(
2421 request: Request,
2422 project_id: str = Path(
2423 ...,
2424 description="The ID of the project.",
2425 ),
2426 sorted_by: ConstraintsSortOptions = Query(
2427 ConstraintsSortOptions.ITERATION_OF_SAMPLING,
2428 description="The option to sort constraints. Defaults to `ITERATION_OF_SAMPLING`.",
2429 ),
2430 sorted_reverse: bool = Query(
2431 False,
2432 description="The option to reverse constraints order. Defaults to `False`.",
2433 ),
2434 # TODO: filter_text
2435 # TODO: limit_size + offset
2436) -> Response:
2437 """
2438 Get HTML constraints page.
2440 Args:
2441 request (Request): The request context.
2442 project_id (str): The ID of the project.
2443 sorted_by (ConstraintsSortOptions, optional): The option to sort constraints. Defaults to `ITERATION_OF_SAMPLING`.
2444 sorted_reverse (bool, optional): The option to reverse constraints order. Defaults to `False`.
2446 Returns:
2447 Response: The requested page.
2448 """
2450 # Return HTML constraints page.
2451 try:
2452 return templates.TemplateResponse(
2453 name="constraints.html",
2454 context={
2455 "request": request,
2456 # Get the project ID.
2457 "project_id": project_id,
2458 # Get the request parameters.
2459 "parameters": {
2460 "without_hidden_constraints": True,
2461 "sorted_by": sorted_by.value,
2462 "sorted_reverse": sorted_reverse,
2463 },
2464 # Get the project metadata (ID, name, creation date).
2465 "metadata": (await get_metadata(project_id=project_id))["metadata"],
2466 # Get the project status (iteration, step name and status, modelization state and conflict).
2467 "status": (await get_status(project_id=project_id))["status"],
2468 # Get the project texts.
2469 "texts": (
2470 await get_texts(
2471 project_id=project_id,
2472 without_deleted_texts=False,
2473 sorted_by=TextsSortOptions.ID,
2474 sorted_reverse=False,
2475 )
2476 )["texts"],
2477 # Get the project constraints.
2478 "constraints": (
2479 await get_constraints(
2480 project_id=project_id,
2481 without_hidden_constraints=True,
2482 sorted_by=sorted_by,
2483 sorted_reverse=sorted_reverse,
2484 )
2485 )["constraints"],
2486 },
2487 status_code=status.HTTP_200_OK,
2488 )
2490 # Case of error: Return HTML error page.
2491 except HTTPException as error:
2492 # Return HTML error page.
2493 return templates.TemplateResponse(
2494 name="error.html",
2495 context={
2496 "request": request,
2497 "status_code": error.status_code,
2498 "detail": error.detail,
2499 },
2500 status_code=error.status_code,
2501 )
2504###
2505### ROUTE: Get HTML constraint annotation page.
2506###
2507@app.get(
2508 "/gui/projects/{project_id}/constraints/{constraint_id}",
2509 tags=["Constraints"],
2510 response_class=Response,
2511 status_code=status.HTTP_200_OK,
2512)
2513async def get_html_constraint_annotation_page(
2514 request: Request,
2515 project_id: str = Path(
2516 ...,
2517 description="The ID of the project.",
2518 ),
2519 constraint_id: str = Path(
2520 ...,
2521 description="The ID of the constraint.",
2522 ),
2523) -> Response:
2524 """
2525 Get HTML constraint annotation page.
2527 Args:
2528 request (Request): The request context.
2529 project_id (str): The ID of the project.
2530 constraint_id (str): The ID of the constraint.
2532 Returns:
2533 Response: The requested page.
2534 """
2536 # Return HTML constraints page.
2537 try:
2538 return templates.TemplateResponse(
2539 name="constraint_annotation.html",
2540 context={
2541 "request": request,
2542 # Get the project ID.
2543 "project_id": project_id,
2544 # Get the constraints ID.
2545 "constraint_id": constraint_id,
2546 # Get the project metadata (ID, name, creation date).
2547 "metadata": (await get_metadata(project_id=project_id))["metadata"],
2548 # Get the project status (iteration, step name and status, modelization state and conflict).
2549 "status": (await get_status(project_id=project_id))["status"],
2550 # Get the project texts.
2551 "texts": (
2552 await get_texts(
2553 project_id=project_id,
2554 without_deleted_texts=False,
2555 sorted_by=TextsSortOptions.ID,
2556 sorted_reverse=False,
2557 )
2558 )["texts"],
2559 "texts_html_escaped": { # TODO: Escape HTML for javascript
2560 text_id: { # Force HTML escape.
2561 key: html.escape(value) if key in {"text_original", "text", "text_preprocessed"} else value
2562 for key, value in text_value.items()
2563 }
2564 for text_id, text_value in (
2565 await get_texts(
2566 project_id=project_id,
2567 without_deleted_texts=False,
2568 sorted_by=TextsSortOptions.ID,
2569 sorted_reverse=False,
2570 )
2571 )["texts"].items()
2572 },
2573 # Get the project constraints.
2574 "constraints": (
2575 await get_constraints(
2576 project_id=project_id,
2577 without_hidden_constraints=False,
2578 sorted_by=ConstraintsSortOptions.TO_ANNOTATE,
2579 sorted_reverse=False,
2580 )
2581 )["constraints"],
2582 # Get the project clustering result.
2583 "clusters": (await get_constrained_clustering_results(project_id=project_id, iteration_id=None))[
2584 "clustering"
2585 ],
2586 # Get the project modelization inference result.
2587 "modelization": (await get_modelization(project_id=project_id))["modelization"],
2588 },
2589 status_code=status.HTTP_200_OK,
2590 )
2592 # Case of error: Return HTML error page.
2593 except HTTPException as error:
2594 # Return HTML error page.
2595 return templates.TemplateResponse(
2596 name="error.html",
2597 context={
2598 "request": request,
2599 "status_code": error.status_code,
2600 "detail": error.detail,
2601 },
2602 status_code=error.status_code,
2603 )
2606# ==============================================================================
2607# DEFINE ROUTES FOR SETTINGS
2608# ==============================================================================
2611###
2612### ROUTE: Get settings.
2613###
2614@app.get(
2615 "/api/projects/{project_id}/settings",
2616 tags=["Settings"],
2617 status_code=status.HTTP_200_OK,
2618)
2619async def get_settings(
2620 project_id: str = Path(
2621 ...,
2622 description="The ID of the project.",
2623 ),
2624 iteration_id: Optional[int] = Query(
2625 None,
2626 description="The ID of project iteration. If `None`, get the current iteration. Defaults to `None`.",
2627 ),
2628 settings_names: List[ICGUISettings] = Query(
2629 [
2630 ICGUISettings.PREPROCESSING,
2631 ICGUISettings.VECTORIZATION,
2632 ICGUISettings.SAMPLING,
2633 ICGUISettings.CLUSTERING,
2634 ],
2635 description="The list of names of requested settings to return. To select multiple settings kinds, use `CTRL + clic`.",
2636 ),
2637) -> Dict[str, Any]:
2638 """
2639 Get settings.
2641 Args:
2642 project_id (str): The ID of the project.
2643 iteration_id (Optional[int], optional): The ID of project iteration. If `None`, get the current iteration. Defaults to `None`.
2644 settings_names (List[ICGUISettings], optional): The list of names of requested settings to return. Defaults to `[ICGUISettings.PREPROCESSING, ICGUISettings.VECTORIZATION, ICGUISettings.SAMPLING, ICGUISettings.CLUSTERING,]`.
2646 Raises:
2647 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
2648 HTTPException: Raises `HTTP_404_NOT_FOUND` if the iteration with id `iteration_id` doesn't exist.
2650 Returns:
2651 Dict[str, Any]: A dictionary that contains settings.
2652 """
2654 # Check project id.
2655 if project_id not in (await get_projects()):
2656 raise HTTPException(
2657 status_code=status.HTTP_404_NOT_FOUND,
2658 detail="The project with id '{project_id_str}' doesn't exist.".format(
2659 project_id_str=str(project_id),
2660 ),
2661 )
2663 # Load settings.
2664 with open(DATA_DIRECTORY / project_id / "settings.json", "r") as settings_fileobject:
2665 project_settings: Dict[str, Dict[str, Any]] = json.load(settings_fileobject)
2667 # Load status file.
2668 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
2669 project_status: Dict[str, Any] = json.load(status_fileobject)
2671 # Get current iteration id if needed.
2672 if iteration_id is None:
2673 iteration_id = project_status["iteration_id"]
2675 # Otherwise check that requested iteration id exist.
2676 if str(iteration_id) not in project_settings.keys():
2677 raise HTTPException(
2678 status_code=status.HTTP_404_NOT_FOUND,
2679 detail="The project with id '{project_id_str}' has no iteration with id '{iteration_id_str}'.".format(
2680 project_id_str=str(project_id),
2681 iteration_id_str=str(iteration_id),
2682 ),
2683 )
2685 # Return the requested settings.
2686 return {
2687 # Get the project ID.
2688 "project_id": project_id,
2689 # Get the iteration ID.
2690 "iteration_id": iteration_id,
2691 # Get the request parameters.
2692 "parameters": {
2693 "settings_names": [settings_name.value for settings_name in settings_names],
2694 },
2695 # Get the settings.
2696 "settings": {
2697 setting_name: settings_value
2698 for setting_name, settings_value in project_settings[str(iteration_id)].items()
2699 if setting_name in settings_names
2700 },
2701 }
2704###
2705### ROUTE: Update settings.
2706###
2707@app.put(
2708 "/api/projects/{project_id}/settings",
2709 tags=["Settings"],
2710 status_code=status.HTTP_201_CREATED,
2711)
2712async def update_settings(
2713 project_id: str = Path(
2714 ...,
2715 description="The ID of the project.",
2716 ),
2717 preprocessing: Optional[PreprocessingSettingsModel] = Body(
2718 None,
2719 description="The settings for data preprocessing. Used during `modelization_update` task. Keep unchanged if empty.",
2720 ),
2721 vectorization: Optional[VectorizationSettingsModel] = Body(
2722 None,
2723 description="The settings for data vectorization. Used during `modelization_update` task. Keep unchanged if empty.",
2724 ),
2725 sampling: Optional[SamplingSettingsModel] = Body(
2726 None,
2727 description="The settings for constraints sampling. Used during `constraints_sampling` task. Keep unchanged if empty.",
2728 ),
2729 clustering: Optional[ClusteringSettingsModel] = Body(
2730 None,
2731 description="The settings for constrained clustering. Used during `constrained_clustering` task. Keep unchanged if empty.",
2732 ),
2733) -> Dict[str, Any]:
2734 """
2735 Update settings.
2737 Args:
2738 project_id (str): The ID of the project.
2739 preprocessing (Optional[PreprocessingSettingsModel], optional): The settings for data preprocessing. Used during `clustering` step. Keep unchanged if empty.. Defaults to None.
2740 vectorization (Optional[VectorizationSettingsModel], optional): The settings for data vectorization. Used during `clustering` step. Keep unchanged if empty.. Defaults to None.
2741 sampling (Optional[SamplingSettingsModel], optional): The settings for constraints sampling. Used during `sampling` step. Keep unchanged if empty.. Defaults to None.
2742 clustering (Optional[ClusteringSettingsModel], optional): The settings for constrained clustering. Used during `clustering` step. Keep unchanged if empty. Defaults to None.
2744 Raises:
2745 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
2746 HTTPException: Raises `HTTP_403_FORBIDDEN` if the status of the project doesn't allow settings modifications.
2747 HTTPException: Raises `HTTP_403_FORBIDDEN` if parameters `preprocessing`, `vectorization`, `sampling` or `clustering` are not expected.
2748 HTTPException: Raises `HTTP_400_BAD_REQUEST` if parameters `preprocessing`, `vectorization`, `sampling` or `clustering` are invalid.
2750 Returns:
2751 Dict[str, Any]: A dictionary that contains the ID of updated settings.
2752 """
2754 # TODO: examples: https://fastapi.tiangolo.com/tutorial/schema-extra-example/#body-with-multiple-examples
2756 # Check project id.
2757 if project_id not in (await get_projects()):
2758 raise HTTPException(
2759 status_code=status.HTTP_404_NOT_FOUND,
2760 detail="The project with id '{project_id_str}' doesn't exist.".format(
2761 project_id_str=str(project_id),
2762 ),
2763 )
2765 # Lock status file in order to check project status for this step.
2766 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
2767 ###
2768 ### Load needed data.
2769 ###
2771 # Load status file.
2772 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject_r:
2773 project_status: Dict[str, Any] = json.load(status_fileobject_r)
2774 iteration_id: int = project_status["iteration_id"]
2776 # Load settings file.
2777 with open(DATA_DIRECTORY / project_id / "settings.json", "r") as settings_fileobject_r:
2778 project_settings: Dict[str, Any] = json.load(settings_fileobject_r)
2780 list_of_updated_settings: List[ICGUISettings] = []
2782 ###
2783 ### Case of preprocessing settings.
2784 ###
2785 if preprocessing is not None:
2786 list_of_updated_settings.append(ICGUISettings.PREPROCESSING)
2788 # Check project status for preprocessing.
2789 if (
2790 project_status["state"] != ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION # noqa: WPS514
2791 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION
2792 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2793 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
2794 ):
2795 raise HTTPException(
2796 status_code=status.HTTP_403_FORBIDDEN,
2797 detail="The 'preprocessing' settings of project with id '{project_id_str}' cant't be modified during this state (state='{state_str}'). No changes have been taken into account.".format(
2798 project_id_str=str(project_id),
2799 state_str=str(project_status["state"]),
2800 ),
2801 )
2803 # Update status by forcing "outdated" status.
2804 if project_status["state"] == ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION:
2805 project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2806 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS:
2807 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2808 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS:
2809 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
2810 #### elif project_status["state"] == ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION:
2811 #### project_status["state"] = ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION
2813 # Update the default settings with the parameters in the request body.
2814 for key_prep, value_prep in preprocessing.to_dict().items():
2815 project_settings[str(iteration_id)]["preprocessing"][key_prep] = value_prep
2817 ###
2818 ### Case of vectorization settings.
2819 ###
2820 if vectorization is not None:
2821 list_of_updated_settings.append(ICGUISettings.VECTORIZATION)
2823 # Check project status for vectorization.
2824 if (
2825 project_status["state"] != ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION # noqa: WPS514
2826 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION
2827 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2828 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
2829 ):
2830 raise HTTPException(
2831 status_code=status.HTTP_403_FORBIDDEN,
2832 detail="The 'vectorization' settings of project with id '{project_id_str}' cant't be modified during this state (state='{state_str}'). No changes have been taken into account.".format(
2833 project_id_str=str(project_id),
2834 state_str=str(project_status["state"]),
2835 ),
2836 )
2838 # Update status by forcing "outdated" status.
2839 if project_status["state"] == ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION:
2840 project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2841 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS:
2842 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2843 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS:
2844 #### project_status["state"] = ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
2845 #### elif project_status["state"] == ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION:
2846 #### project_status["state"] = ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION
2848 # Update the default settings with the parameters in the request body.
2849 for key_vect, value_vect in vectorization.to_dict().items():
2850 project_settings[str(iteration_id)]["vectorization"][key_vect] = value_vect
2852 ###
2853 ### Case of sampling settings.
2854 ###
2855 if sampling is not None:
2856 list_of_updated_settings.append(ICGUISettings.SAMPLING)
2858 # Check project status for sampling.
2859 if project_status["state"] != ICGUIStates.SAMPLING_TODO:
2860 raise HTTPException(
2861 status_code=status.HTTP_403_FORBIDDEN,
2862 detail="The 'sampling' settings of project with id '{project_id_str}' cant't be modified during this state (state='{state_str}'). No changes have been taken into account.".format(
2863 project_id_str=str(project_id),
2864 state_str=str(project_status["state"]),
2865 ),
2866 )
2868 # Update the default settings with the parameters in the request body.
2869 for key_sampl, value_sampl in sampling.to_dict().items():
2870 project_settings[str(iteration_id)]["sampling"][key_sampl] = value_sampl
2872 ###
2873 ### Case of clustering settings.
2874 ###
2875 if clustering is not None:
2876 list_of_updated_settings.append(ICGUISettings.CLUSTERING)
2878 # Check project status for clustering.
2879 if (
2880 project_status["state"] != ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION # noqa: WPS514
2881 and project_status["state"] != ICGUIStates.INITIALIZATION_WITH_PENDING_MODELIZATION
2882 and project_status["state"] != ICGUIStates.INITIALIZATION_WITH_WORKING_MODELIZATION
2883 and project_status["state"] != ICGUIStates.SAMPLING_TODO
2884 and project_status["state"] != ICGUIStates.SAMPLING_PENDING
2885 and project_status["state"] != ICGUIStates.SAMPLING_WORKING
2886 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION
2887 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
2888 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_PENDING_MODELIZATION_WITHOUT_CONFLICTS
2889 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_WORKING_MODELIZATION_WITHOUT_CONFLICTS
2890 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
2891 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_PENDING_MODELIZATION_WITH_CONFLICTS
2892 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_WORKING_MODELIZATION_WITH_CONFLICTS
2893 and project_status["state"] != ICGUIStates.CLUSTERING_TODO
2894 ):
2895 raise HTTPException(
2896 status_code=status.HTTP_403_FORBIDDEN,
2897 detail="The 'clustering' settings of project with id '{project_id_str}' cant't be modified during this state (state='{state_str}'). No changes have been taken into account.".format(
2898 project_id_str=str(project_id),
2899 state_str=str(project_status["state"]),
2900 ),
2901 )
2903 # Update the default settings with the parameters in the request body.
2904 for key_clus, value_clus in clustering.to_dict().items():
2905 project_settings[str(iteration_id)]["clustering"][key_clus] = value_clus
2907 ###
2908 ### Store updated data.
2909 ###
2911 # Store updated status in file.
2912 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
2913 json.dump(project_status, status_fileobject_w, indent=4)
2915 # Store updated settings in file.
2916 with open(DATA_DIRECTORY / project_id / "settings.json", "w") as settings_fileobject_w:
2917 json.dump(project_settings, settings_fileobject_w, indent=4)
2919 ###
2920 ### Return statement.
2921 ###
2922 return {
2923 "project_id": project_id,
2924 "detail": "The project with id '{project_id_str}' has updated the following settings: {settings_str}.".format(
2925 project_id_str=str(project_id),
2926 settings_str=", ".join(list_of_updated_settings),
2927 ),
2928 }
2931###
2932### ROUTE: Get HTML settings page.
2933###
2934@app.get(
2935 "/gui/projects/{project_id}/settings",
2936 tags=["Settings"],
2937 response_class=Response,
2938 status_code=status.HTTP_200_OK,
2939)
2940async def get_html_settings_page(
2941 request: Request,
2942 project_id: str = Path(
2943 ...,
2944 description="The ID of the project.",
2945 ),
2946 iteration_id: Optional[int] = Query(
2947 None,
2948 description="The ID of project iteration. If `None`, get the current iteration. Defaults to `None`.",
2949 ),
2950 settings_names: List[ICGUISettings] = Query(
2951 [
2952 ICGUISettings.PREPROCESSING,
2953 ICGUISettings.VECTORIZATION,
2954 ICGUISettings.SAMPLING,
2955 ICGUISettings.CLUSTERING,
2956 ],
2957 description="The list of names of requested settings to return. To select multiple settings kinds, use `CTRL + clic`.",
2958 ),
2959) -> Response:
2960 """
2961 Get HTML settings page.
2963 Args:
2964 request (Request): The request context.
2965 project_id (str): The ID of the project.
2966 iteration_id (Optional[int], optional): The ID of project iteration. If `None`, get the current iteration. Defaults to `None`.
2967 settings_names (List[ICGUISettings], optional): The list of names of requested settings to return. Defaults to `[ICGUISettings.PREPROCESSING, ICGUISettings.VECTORIZATION, ICGUISettings.SAMPLING, ICGUISettings.CLUSTERING,]`.
2969 Returns:
2970 Response: The requested page.
2971 """
2973 # Return HTML project home page.
2974 try: # noqa: WPS229 (too long try body)
2975 project_status: Dict[str, Any] = (await get_status(project_id=project_id))["status"]
2976 if iteration_id is None:
2977 iteration_id = project_status["iteration_id"]
2979 return templates.TemplateResponse(
2980 name="settings.html",
2981 context={
2982 "request": request,
2983 # Get the project ID.
2984 "project_id": project_id,
2985 # Get the iteration ID.
2986 "iteration_id": iteration_id,
2987 # Get the request parameters.
2988 "parameters": {
2989 "settings_names": [settings_name.value for settings_name in settings_names],
2990 },
2991 # Get the project metadata (ID, name, creation date).
2992 "metadata": (await get_metadata(project_id=project_id))["metadata"],
2993 # Get the project status (iteration, step name and status, modelization state and conflict).
2994 "status": project_status,
2995 # Get the project settings (preprocessing, vectorization, sampling, clustering).
2996 "settings": (
2997 await get_settings(project_id=project_id, iteration_id=iteration_id, settings_names=settings_names)
2998 )["settings"],
2999 # Get navigation information.
3000 "navigation": {
3001 "previous": None if (iteration_id == 0) else iteration_id - 1,
3002 "next": None if (iteration_id == project_status["iteration_id"]) else (iteration_id + 1),
3003 },
3004 },
3005 status_code=status.HTTP_200_OK,
3006 )
3008 # Case of error: Return HTML error page.
3009 except HTTPException as error:
3010 # Return HTML error page.
3011 return templates.TemplateResponse(
3012 name="error.html",
3013 context={
3014 "request": request,
3015 "status_code": error.status_code,
3016 "detail": error.detail,
3017 },
3018 status_code=error.status_code,
3019 )
3022# ==============================================================================
3023# DEFINE ROUTES FOR MODELIZATION
3024# ==============================================================================
3027###
3028### ROUTE: Get modelization inference.
3029###
3030@app.get(
3031 "/api/projects/{project_id}/modelization",
3032 tags=["Data modelization"],
3033 status_code=status.HTTP_200_OK,
3034)
3035async def get_modelization(
3036 project_id: str = Path(
3037 ...,
3038 description="The ID of the project.",
3039 ),
3040) -> Dict[str, Any]:
3041 """
3042 Get modelization inference.
3044 Args:
3045 project_id (str, optional): The ID of the project.
3047 Raises:
3048 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
3050 Returns:
3051 Dict[str, Any]: A dictionary that contains modelization inference result.
3052 """
3054 # Check project id.
3055 if project_id not in (await get_projects()):
3056 raise HTTPException(
3057 status_code=status.HTTP_404_NOT_FOUND,
3058 detail="The project with id '{project_id_str}' doesn't exist.".format(
3059 project_id_str=str(project_id),
3060 ),
3061 )
3063 # Load the modelization inference results.
3064 with open(DATA_DIRECTORY / project_id / "modelization.json", "r") as modelization_fileobject:
3065 # Return the project modelization inference.
3066 return {
3067 "project_id": project_id,
3068 "modelization": json.load(modelization_fileobject),
3069 }
3072###
3073### ROUTE: Get 2D and 3D vectors.
3074###
3075@app.get(
3076 "/api/projects/{project_id}/vectors",
3077 tags=["Data modelization"],
3078 status_code=status.HTTP_200_OK,
3079)
3080async def get_vectors(
3081 project_id: str = Path(
3082 ...,
3083 description="The ID of the project.",
3084 ),
3085) -> Dict[str, Any]:
3086 """
3087 Get 2D and 3D vectors.
3089 Args:
3090 project_id (str, optional): The ID of the project.
3092 Raises:
3093 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
3094 HTTPException: Raises `HTTP_404_NOT_FOUND` if the iteration with id `iteration_id` doesn't exist.
3095 HTTPException: Raises `HTTP_403_FORBIDDEN` if the status of the project hasn't completed its clustering step.
3097 Returns:
3098 Dict[str, Any]: A dictionary that contains clustering result.
3099 """
3101 # Check project id.
3102 if project_id not in (await get_projects()):
3103 raise HTTPException(
3104 status_code=status.HTTP_404_NOT_FOUND,
3105 detail="The project with id '{project_id_str}' doesn't exist.".format(
3106 project_id_str=str(project_id),
3107 ),
3108 )
3110 # Load status file.
3111 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
3112 project_status: Dict[str, Any] = json.load(status_fileobject)
3114 # Check project status.
3115 if (
3116 project_status["state"] != ICGUIStates.SAMPLING_TODO # noqa: WPS514
3117 and project_status["state"] != ICGUIStates.SAMPLING_PENDING
3118 and project_status["state"] != ICGUIStates.SAMPLING_WORKING
3119 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_UPTODATE_MODELIZATION
3120 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
3121 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_PENDING_MODELIZATION_WITHOUT_CONFLICTS
3122 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_WORKING_MODELIZATION_WITHOUT_CONFLICTS
3123 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
3124 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_PENDING_MODELIZATION_WITH_CONFLICTS
3125 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_WORKING_MODELIZATION_WITH_CONFLICTS
3126 and project_status["state"] != ICGUIStates.CLUSTERING_TODO
3127 and project_status["state"] != ICGUIStates.CLUSTERING_PENDING
3128 and project_status["state"] != ICGUIStates.CLUSTERING_WORKING
3129 and project_status["state"] != ICGUIStates.ITERATION_END
3130 ):
3131 raise HTTPException(
3132 status_code=status.HTTP_403_FORBIDDEN,
3133 detail="The project with id '{project_id_str}' hasn't completed its modelization update step.".format(
3134 project_id_str=str(project_id),
3135 ),
3136 )
3138 # Load the 2D vectors.
3139 with open(DATA_DIRECTORY / project_id / "vectors_2D.json", "r") as vectors_2D_fileobject:
3140 vectors_2D: Dict[str, Dict[str, float]] = json.load(vectors_2D_fileobject) # noqa: S301 # Usage of Pickle
3142 # Load the 3D vectors.
3143 with open(DATA_DIRECTORY / project_id / "vectors_3D.json", "r") as vectors_3D_fileobject:
3144 vectors_3D: Dict[str, Dict[str, float]] = json.load(vectors_3D_fileobject) # noqa: S301 # Usage of Pickle
3146 # Return the project vectors.
3147 return {
3148 "project_id": project_id,
3149 "vectors_2d": vectors_2D,
3150 "vectors_3d": vectors_3D,
3151 }
3154###
3155### ROUTE: Prepare modelization update task.
3156###
3157@app.post(
3158 "/api/projects/{project_id}/modelization",
3159 tags=["Data modelization"],
3160 status_code=status.HTTP_202_ACCEPTED,
3161)
3162async def prepare_modelization_update_task(
3163 background_tasks: BackgroundTasks,
3164 project_id: str = Path(
3165 ...,
3166 description="The ID of the project.",
3167 ),
3168) -> Dict[str, Any]:
3169 """
3170 Prepare modelization update task.
3172 Args:
3173 background_tasks (BackgroundTasks): A background task to run after the return statement.
3174 project_id (str): The ID of the project.
3176 Raises:
3177 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
3178 HTTPException: Raises `HTTP_403_FORBIDDEN` if the current status of the project doesn't allow the preparation of modelization update task.
3180 Returns:
3181 Dict[str, Any]: A dictionary that contains the confirmation of the preparation of modelization update task.
3182 """
3184 # Check project id.
3185 if project_id not in (await get_projects()):
3186 raise HTTPException(
3187 status_code=status.HTTP_404_NOT_FOUND,
3188 detail="The project with id '{project_id_str}' doesn't exist.".format(
3189 project_id_str=str(project_id),
3190 ),
3191 )
3193 # Lock status file in order to check project status for this step.
3194 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
3195 ###
3196 ### Load needed data.
3197 ###
3199 # Load status file.
3200 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
3201 project_status: Dict[str, Any] = json.load(status_fileobject)
3203 ###
3204 ### Check parameters.
3205 ###
3207 # Check status.
3208 if (
3209 project_status["state"] != ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION # noqa: WPS514
3210 and project_status["state"] != ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITHOUT_MODELIZATION
3211 and project_status["state"] != ICGUIStates.IMPORT_AT_ANNOTATION_STEP_WITHOUT_MODELIZATION
3212 and project_status["state"] != ICGUIStates.IMPORT_AT_CLUSTERING_STEP_WITHOUT_MODELIZATION
3213 and project_status["state"] != ICGUIStates.IMPORT_AT_ITERATION_END_WITHOUT_MODELIZATION
3214 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS
3215 and project_status["state"] != ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS
3216 ):
3217 raise HTTPException(
3218 status_code=status.HTTP_403_FORBIDDEN,
3219 detail="The project with id '{project_id_str}' doesn't allow the preparation of modelization update task during this state (state='{state_str}').".format(
3220 project_id_str=str(project_id),
3221 state_str=str(project_status["state"]),
3222 ),
3223 )
3225 ###
3226 ### Update data.
3227 ###
3229 # Update status by forcing "pending" status.
3230 if project_status["state"] == ICGUIStates.INITIALIZATION_WITHOUT_MODELIZATION:
3231 project_status["state"] = ICGUIStates.INITIALIZATION_WITH_PENDING_MODELIZATION
3232 elif project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITHOUT_MODELIZATION:
3233 project_status["state"] = ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_PENDING_MODELIZATION
3234 elif project_status["state"] == ICGUIStates.IMPORT_AT_ANNOTATION_STEP_WITHOUT_MODELIZATION:
3235 project_status["state"] = ICGUIStates.IMPORT_AT_ANNOTATION_STEP_WITH_PENDING_MODELIZATION
3236 elif project_status["state"] == ICGUIStates.IMPORT_AT_CLUSTERING_STEP_WITHOUT_MODELIZATION:
3237 project_status["state"] = ICGUIStates.IMPORT_AT_CLUSTERING_STEP_WITH_PENDING_MODELIZATION
3238 elif project_status["state"] == ICGUIStates.IMPORT_AT_ITERATION_END_WITHOUT_MODELIZATION:
3239 project_status["state"] = ICGUIStates.IMPORT_AT_ITERATION_END_WITH_PENDING_MODELIZATION
3240 elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITHOUT_CONFLICTS:
3241 project_status["state"] = ICGUIStates.ANNOTATION_WITH_PENDING_MODELIZATION_WITHOUT_CONFLICTS
3242 #### elif project_status["state"] == ICGUIStates.ANNOTATION_WITH_OUTDATED_MODELIZATION_WITH_CONFLICTS:
3243 else:
3244 project_status["state"] = ICGUIStates.ANNOTATION_WITH_PENDING_MODELIZATION_WITH_CONFLICTS
3246 # Prepare status by initializing "task" status.
3247 project_status["task"] = {
3248 "progression": 1,
3249 "detail": "Waiting for background task allocation...",
3250 }
3252 ###
3253 ### Store updated data.
3254 ###
3256 # Store updated status in file.
3257 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
3258 json.dump(project_status, status_fileobject_w, indent=4)
3260 ###
3261 ### Launch backgroundtask.
3262 ###
3264 # Add the background task.
3265 background_tasks.add_task(
3266 func=backgroundtasks.run_modelization_update_task,
3267 project_id=project_id,
3268 )
3270 # Return statement.
3271 return { # pragma: no cover (need radis and worder)
3272 "project_id": project_id,
3273 "detail": "In project with id '{project_id_str}', the modelization update task has been requested and is waiting for a background task.".format(
3274 project_id_str=str(project_id),
3275 ),
3276 }
3279# ==============================================================================
3280# DEFINE ROUTES FOR CONSTRAINTS SAMPLING
3281# ==============================================================================
3284###
3285### ROUTE: Get constraints sampling results.
3286###
3287@app.get(
3288 "/api/projects/{project_id}/sampling",
3289 tags=["Constraints sampling"],
3290 status_code=status.HTTP_200_OK,
3291)
3292async def get_constraints_sampling_results(
3293 project_id: str = Path(
3294 ...,
3295 description="The ID of the project.",
3296 ),
3297 iteration_id: Optional[int] = Query(
3298 None,
3299 description="The ID of project iteration. If `None`, get the current iteration. Defaults to `None`.",
3300 ),
3301) -> Dict[str, Any]:
3302 """
3303 Get constraints sampling results.
3305 Args:
3306 project_id (str, optional): The ID of the project.
3307 iteration_id (Optional[int], optional): The ID of project iteration. If `None`, get the current iteration. Defaults to `None`.
3309 Raises:
3310 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
3311 HTTPException: Raises `HTTP_404_NOT_FOUND` if the iteration with id `iteration_id` doesn't exist.
3312 HTTPException: Raises `HTTP_403_FORBIDDEN` if the status of the project hasn't completed its sampling step.
3314 Returns:
3315 Dict[str, Any]: A dictionary that contains sampling result.
3316 """
3318 # Check project id.
3319 if project_id not in (await get_projects()):
3320 raise HTTPException(
3321 status_code=status.HTTP_404_NOT_FOUND,
3322 detail="The project with id '{project_id_str}' doesn't exist.".format(
3323 project_id_str=str(project_id),
3324 ),
3325 )
3327 # Load settings.
3328 with open(DATA_DIRECTORY / project_id / "settings.json", "r") as settings_fileobject:
3329 project_settings: Dict[str, Dict[str, Any]] = json.load(settings_fileobject)
3331 # Load status file.
3332 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
3333 project_status: Dict[str, Any] = json.load(status_fileobject)
3335 # Get current iteration id if needed.
3336 if iteration_id is None:
3337 if project_status["iteration_id"] == 0:
3338 iteration_id = 0
3339 elif (
3340 project_status["state"] == ICGUIStates.SAMPLING_TODO # noqa: WPS514
3341 or project_status["state"] == ICGUIStates.SAMPLING_PENDING
3342 or project_status["state"] == ICGUIStates.SAMPLING_WORKING
3343 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITHOUT_MODELIZATION
3344 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_PENDING_MODELIZATION
3345 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_WORKING_MODELIZATION
3346 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_ERRORS
3347 ):
3348 iteration_id = project_status["iteration_id"] - 1
3349 else:
3350 iteration_id = project_status["iteration_id"]
3352 # Case of iteration `0`.
3353 if iteration_id == 0:
3354 raise HTTPException(
3355 status_code=status.HTTP_403_FORBIDDEN,
3356 detail="The iteration `0` has no sampling step.",
3357 )
3359 # Check project status.
3360 if iteration_id == project_status["iteration_id"] and (
3361 project_status["state"] == ICGUIStates.SAMPLING_TODO # noqa: WPS514
3362 or project_status["state"] == ICGUIStates.SAMPLING_PENDING
3363 or project_status["state"] == ICGUIStates.SAMPLING_WORKING
3364 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITHOUT_MODELIZATION
3365 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_PENDING_MODELIZATION
3366 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_WORKING_MODELIZATION
3367 or project_status["state"] == ICGUIStates.IMPORT_AT_SAMPLING_STEP_WITH_ERRORS
3368 ):
3369 raise HTTPException(
3370 status_code=status.HTTP_403_FORBIDDEN,
3371 detail="The project with id '{project_id_str}' hasn't completed its sampling step on iteration '{iteration_id_str}'.".format(
3372 project_id_str=str(project_id),
3373 iteration_id_str=str(iteration_id),
3374 ),
3375 )
3377 # Otherwise check that requested iteration id exist.
3378 if str(iteration_id) not in project_settings.keys():
3379 raise HTTPException(
3380 status_code=status.HTTP_404_NOT_FOUND,
3381 detail="The project with id '{project_id_str}' has no iteration with id '{iteration_id_str}'.".format(
3382 project_id_str=str(project_id),
3383 iteration_id_str=str(iteration_id),
3384 ),
3385 )
3387 # Load the sampling results.
3388 with open(DATA_DIRECTORY / project_id / "sampling.json", "r") as sampling_fileobject:
3389 # Return the project sampling.
3390 return {
3391 "project_id": project_id,
3392 "iteration_id": iteration_id,
3393 "sampling": json.load(sampling_fileobject)[str(iteration_id)],
3394 }
3397###
3398### ROUTE: Prepare constraints sampling task.
3399###
3400@app.post(
3401 "/api/projects/{project_id}/sampling",
3402 tags=["Constraints sampling"],
3403 status_code=status.HTTP_202_ACCEPTED,
3404)
3405async def prepare_constraints_sampling_task(
3406 background_tasks: BackgroundTasks,
3407 project_id: str = Path(
3408 ...,
3409 description="The ID of the project.",
3410 ),
3411) -> Dict[str, Any]:
3412 """
3413 Prepare constraints sampling task.
3415 Args:
3416 background_tasks (BackgroundTasks): A background task to run after the return statement.
3417 project_id (str): The ID of the project.
3419 Raises:
3420 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
3421 HTTPException: Raises `HTTP_403_FORBIDDEN` if the current status of the project doesn't allow the preparation of constraints sampling task.
3423 Returns:
3424 Dict[str, Any]: A dictionary that contains the confirmation of the preparation of constraints sampling task.
3425 """
3427 # Check project id.
3428 if project_id not in (await get_projects()):
3429 raise HTTPException(
3430 status_code=status.HTTP_404_NOT_FOUND,
3431 detail="The project with id '{project_id_str}' doesn't exist.".format(
3432 project_id_str=str(project_id),
3433 ),
3434 )
3436 # Lock status file in order to check project status for this step.
3437 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
3438 # Load status file.
3439 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
3440 project_status: Dict[str, Any] = json.load(status_fileobject)
3442 # Check status.
3443 if project_status["state"] != ICGUIStates.SAMPLING_TODO:
3444 raise HTTPException(
3445 status_code=status.HTTP_403_FORBIDDEN,
3446 detail="The project with id '{project_id_str}' doesn't allow the preparation of constraints sampling task during this state (state='{state_str}').".format(
3447 project_id_str=str(project_id),
3448 state_str=str(project_status["state"]),
3449 ),
3450 )
3452 ###
3453 ### Update data.
3454 ###
3456 # Update status by forcing "pending" status.
3457 project_status["state"] = ICGUIStates.SAMPLING_PENDING
3459 # Prepare status by initializing "task" status.
3460 project_status["task"] = {
3461 "progression": 1,
3462 "detail": "Waiting for background task allocation...",
3463 }
3465 ###
3466 ### Store updated data.
3467 ###
3469 # Store updated status in file.
3470 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
3471 json.dump(project_status, status_fileobject_w, indent=4)
3473 ###
3474 ### Launch backgroundtask.
3475 ###
3477 # Add the background task.
3478 background_tasks.add_task(
3479 func=backgroundtasks.run_constraints_sampling_task,
3480 project_id=project_id,
3481 )
3483 # Return statement.
3484 return { # pragma: no cover (need radis and worder)
3485 "project_id": project_id,
3486 "detail": "In project with id '{project_id_str}', the constraints sampling task has been requested and is waiting for a background task.".format(
3487 project_id_str=str(project_id),
3488 ),
3489 }
3492# ==============================================================================
3493# DEFINE ROUTES FOR CONSTRAINED CLUSTERING
3494# ==============================================================================
3497###
3498### ROUTE: Get constrained clustering results.
3499###
3500@app.get(
3501 "/api/projects/{project_id}/clustering",
3502 tags=["Constrained clustering"],
3503 status_code=status.HTTP_200_OK,
3504)
3505async def get_constrained_clustering_results(
3506 project_id: str = Path(
3507 ...,
3508 description="The ID of the project.",
3509 ),
3510 iteration_id: Optional[int] = Query(
3511 None,
3512 description="The ID of project iteration. If `None`, get the current iteration. Defaults to `None`.",
3513 ),
3514) -> Dict[str, Any]:
3515 """
3516 Get constrained clustering results.
3518 Args:
3519 project_id (str, optional): The ID of the project.
3520 iteration_id (Optional[int], optional): The ID of project iteration. If `None`, get the current iteration. Defaults to `None`.
3522 Raises:
3523 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
3524 HTTPException: Raises `HTTP_404_NOT_FOUND` if the iteration with id `iteration_id` doesn't exist.
3525 HTTPException: Raises `HTTP_403_FORBIDDEN` if the status of the project hasn't completed its clustering step.
3527 Returns:
3528 Dict[str, Any]: A dictionary that contains clustering result.
3529 """
3531 # Check project id.
3532 if project_id not in (await get_projects()):
3533 raise HTTPException(
3534 status_code=status.HTTP_404_NOT_FOUND,
3535 detail="The project with id '{project_id_str}' doesn't exist.".format(
3536 project_id_str=str(project_id),
3537 ),
3538 )
3540 # Load status file.
3541 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
3542 project_status: Dict[str, Any] = json.load(status_fileobject)
3544 # Load clustering.
3545 with open(DATA_DIRECTORY / project_id / "clustering.json", "r") as clustering_fileobject:
3546 project_clustering: Dict[str, Dict[str, Any]] = json.load(clustering_fileobject)
3548 # Set iteration id if needed.
3549 if iteration_id is None:
3550 if (
3551 project_status["iteration_id"] == 0
3552 or project_status["state"] == ICGUIStates.ITERATION_END
3553 or project_status["state"] == ICGUIStates.IMPORT_AT_ITERATION_END_WITHOUT_MODELIZATION
3554 or project_status["state"] == ICGUIStates.IMPORT_AT_ITERATION_END_WITH_PENDING_MODELIZATION
3555 or project_status["state"] == ICGUIStates.IMPORT_AT_ITERATION_END_WITH_WORKING_MODELIZATION
3556 or project_status["state"] == ICGUIStates.IMPORT_AT_ITERATION_END_WITH_ERRORS
3557 ):
3558 iteration_id = project_status["iteration_id"]
3559 else:
3560 iteration_id = project_status["iteration_id"] - 1
3562 # Check project status.
3563 if (
3564 iteration_id == project_status["iteration_id"]
3565 and project_status["state"] != ICGUIStates.ITERATION_END
3566 and project_status["state"] != ICGUIStates.IMPORT_AT_ITERATION_END_WITHOUT_MODELIZATION
3567 and project_status["state"] != ICGUIStates.IMPORT_AT_ITERATION_END_WITH_PENDING_MODELIZATION
3568 and project_status["state"] != ICGUIStates.IMPORT_AT_ITERATION_END_WITH_WORKING_MODELIZATION
3569 and project_status["state"] != ICGUIStates.IMPORT_AT_ITERATION_END_WITH_ERRORS
3570 ):
3571 raise HTTPException(
3572 status_code=status.HTTP_403_FORBIDDEN,
3573 detail="The project with id '{project_id_str}' hasn't completed its clustering step on iteration '{iteration_id_str}'.".format(
3574 project_id_str=str(project_id),
3575 iteration_id_str=str(iteration_id),
3576 ),
3577 )
3579 # Otherwise check that requested iteration id exist.
3580 if str(iteration_id) not in project_clustering.keys():
3581 raise HTTPException(
3582 status_code=status.HTTP_404_NOT_FOUND,
3583 detail="The project with id '{project_id_str}' has no iteration with id '{iteration_id_str}'.".format(
3584 project_id_str=str(project_id),
3585 iteration_id_str=str(iteration_id),
3586 ),
3587 )
3589 # Return the project clustering.
3590 return {
3591 "project_id": project_id,
3592 "iteration_id": iteration_id,
3593 "clustering": project_clustering[str(iteration_id)],
3594 }
3597###
3598### ROUTE: Prepare constrained clustering task.
3599###
3600@app.post(
3601 "/api/projects/{project_id}/clustering",
3602 tags=["Constrained clustering"],
3603 status_code=status.HTTP_202_ACCEPTED,
3604)
3605async def prepare_constrained_clustering_task(
3606 background_tasks: BackgroundTasks,
3607 project_id: str = Path(
3608 ...,
3609 description="The ID of the project.",
3610 ),
3611) -> Dict[str, Any]:
3612 """
3613 Prepare constrained clustering task.
3615 Args:
3616 background_tasks (BackgroundTasks): A background task to run after the return statement.
3617 project_id (str): The ID of the project.
3619 Raises:
3620 HTTPException: Raises `HTTP_404_NOT_FOUND` if the project with id `project_id` doesn't exist.
3621 HTTPException: Raises `HTTP_403_FORBIDDEN` if the current status of the project doesn't allow the preparation of constrained clustering task.
3622 HTTPException: Raises `HTTP_504_GATEWAY_TIMEOUT` if the task can't be prepared.
3624 Returns:
3625 Dict[str, Any]: A dictionary that contains the confirmation of the preparation of constrained clustering task.
3626 """
3628 # Check project id.
3629 if project_id not in (await get_projects()):
3630 raise HTTPException(
3631 status_code=status.HTTP_404_NOT_FOUND,
3632 detail="The project with id '{project_id_str}' doesn't exist.".format(
3633 project_id_str=str(project_id),
3634 ),
3635 )
3637 # Lock status file in order to check project status for this step.
3638 with FileLock(str(DATA_DIRECTORY / project_id / "status.json.lock")):
3639 ###
3640 ### Load needed data.
3641 ###
3643 # Load status file.
3644 with open(DATA_DIRECTORY / project_id / "status.json", "r") as status_fileobject:
3645 project_status: Dict[str, Any] = json.load(status_fileobject)
3647 ###
3648 ### Check parameters.
3649 ###
3651 # Check status.
3652 if project_status["state"] != ICGUIStates.CLUSTERING_TODO:
3653 raise HTTPException(
3654 status_code=status.HTTP_403_FORBIDDEN,
3655 detail="The project with id '{project_id_str}' doesn't allow the preparation of constrained clustering task during this state (state='{state_str}').".format(
3656 project_id_str=str(project_id),
3657 state_str=str(project_status["state"]),
3658 ),
3659 )
3661 ###
3662 ### Update data.
3663 ###
3665 # Update status by forcing "pending" status.
3666 project_status["state"] = ICGUIStates.CLUSTERING_PENDING
3668 # Prepare status by initializing "task" status.
3669 project_status["task"] = {
3670 "progression": 1,
3671 "detail": "Waiting for background task allocation...",
3672 }
3674 ###
3675 ### Store updated data.
3676 ###
3678 # Store updated status in file.
3679 with open(DATA_DIRECTORY / project_id / "status.json", "w") as status_fileobject_w:
3680 json.dump(project_status, status_fileobject_w, indent=4)
3682 ###
3683 ### Launch backgroundtask.
3684 ###
3686 # Add the background task.
3687 background_tasks.add_task(
3688 func=backgroundtasks.run_constrained_clustering_task,
3689 project_id=project_id,
3690 )
3692 # Return statement.
3693 return { # pragma: no cover (need radis and worder)
3694 "project_id": project_id,
3695 "detail": "In project with id '{project_id_str}', the constrained clustering task has been requested and is waiting for a background task.".format(
3696 project_id_str=str(project_id),
3697 ),
3698 }