From 6fb8892be65d15bbed99f4f83d7dfdec8d992980 Mon Sep 17 00:00:00 2001 From: grololo06 Date: Thu, 7 Oct 2021 08:44:06 +0200 Subject: [PATCH] #24: Workaround https://github.com/tiangolo/fastapi/issues/684 by setting info in models. --- py/API_models/crud.py | 230 ++++++++++---- py/API_models/exports.py | 2 + py/API_models/helpers/TypedDictToModel.py | 9 +- py/API_models/imports.py | 6 + py/API_models/login.py | 7 +- py/API_models/objects.py | 173 +++++++---- py/API_models/prediction.py | 26 +- py/API_models/subset.py | 3 + py/main.py | 356 ++++++++++++---------- 9 files changed, 512 insertions(+), 300 deletions(-) diff --git a/py/API_models/crud.py b/py/API_models/crud.py index 63a5d655..aa490bb9 100644 --- a/py/API_models/crud.py +++ b/py/API_models/crud.py @@ -10,8 +10,8 @@ from BO.ColumnUpdate import ColUpdateList from BO.DataLicense import LicenseEnum -from BO.Project import ProjectUserStats, ProjectSetColumnStats from BO.Job import DBJobStateEnum +from BO.Project import ProjectUserStats, ProjectSetColumnStats from BO.Sample import SampleTaxoStats from DB import User, Project, Sample, Acquisition, Process, Job from DB.Acquisition import ACQUISITION_FREE_COLUMNS @@ -34,9 +34,13 @@ "name": Field(title="Name", description="User's full name, as text.", default=None, example="userName"), "organisation": Field(title="Organisation", description="User's organisation name, as text.", default=None, example="Oceanographic Laboratory of Villefranche sur Mer - LOV"), - "active": Field(title="Account status", description="Whether the user is still active.", default=None, example=True), - "country": Field(title="Country", description="The country name, as text (but chosen in a consistent list).", default=None, example="France"), - "usercreationdate": Field(title="User creation date", description="The date of creation of the user, as text formatted according to the ISO 8601 standard.", default=None, + "active": Field(title="Account status", description="Whether the user is still active.", default=None, + example=True), + "country": Field(title="Country", description="The country name, as text (but chosen in a consistent list).", + default=None, example="France"), + "usercreationdate": Field(title="User creation date", + description="The date of creation of the user, as text formatted according to the ISO 8601 standard.", + default=None, example="2020-11-05T12:31:48.299713"), "usercreationreason": Field(title="User creation reason", description="Paragraph describing the usage of EcoTaxa made by the user.", @@ -87,7 +91,8 @@ class ProjectSummaryModel(BaseModel): _DBSampleDescription = { "sampleid": Field(title="Sample Id", description="The sample Id", example=100), "projid": Field(title="Project Id", description="The project Id", example=4), - "orig_id": Field(title="Original id", description="Original sample ID from initial TSV load", example="dewex_leg2_19"), + "orig_id": Field(title="Original id", description="Original sample ID from initial TSV load", + example="dewex_leg2_19"), "latitude": Field(title="Latitude", description="The latitude", example=42.0231666666667), "longitude": Field(title="Longitude", description="The longitude", example=4.71766666666667), "dataportal_descriptor": Field(title="Dataportal descriptor", description="", example=""), @@ -100,7 +105,8 @@ class ProjectSummaryModel(BaseModel): "acquisid": Field(title="Acquisition Id", description="The acquisition Id", example=144, default=None), "acq_sample_id": Field(title="Acquisition sample Id", description="The acquisition sample Id", example=1039, default=None), - "orig_id": Field(title="Original id", description="Original acquisition ID from initial TSV load", example="uvp5_station1_cast1b", default=None), + "orig_id": Field(title="Original id", description="Original acquisition ID from initial TSV load", + example="uvp5_station1_cast1b", default=None), "instrument": Field(title="Instrument", description="Instrument used", example="uvp5", default=None), } @@ -108,8 +114,9 @@ class ProjectSummaryModel(BaseModel): exclude=["t%02d" % i for i in range(1, ACQUISITION_FREE_COLUMNS)], field_infos=_DBAcquisitionDescription) _DBProcessDescription = { - "processid" : Field(title="Process id", description="The process Id", example=1000), - "orig_id" : Field(title="Original id", description="Original process ID from initial TSV load", example="zooprocess_045") + "processid": Field(title="Process id", description="The process Id", example=1000), + "orig_id": Field(title="Original id", description="Original process ID from initial TSV load", + example="zooprocess_045") } _ProcessModelFromDB = sqlalchemy_to_pydantic(Process, exclude=["t%02d" % i for i in range(1, PROCESS_FREE_COLUMNS)], @@ -201,10 +208,13 @@ class SampleModel(_SampleModelFromDB): # type:ignore description="Free columns from sample mapping in project", default={}, example={"flash_delay": "t01"}) + _DBSampleTaxoStatsDescription = { "sample_id": Field(title="Sample id", description="The sample id"), - "used_taxa": Field(title="Used taxa", description="The taxa/category ids used inside the sample. -1 for unclassified objects"), - "nb_unclassified": Field(title="Number unclassified", description="The number of unclassified objects inside the sample"), + "used_taxa": Field(title="Used taxa", + description="The taxa/category ids used inside the sample. -1 for unclassified objects"), + "nb_unclassified": Field(title="Number unclassified", + description="The number of unclassified objects inside the sample"), "nb_validated": Field(title="Number validated", description="The number of validated objects inside the sample"), "nb_dubious": Field(title="Number dubious", description="The number of dubious objects inside the sample"), "nb_predicted": Field(title="Number predicted", description="The number of predicted objects inside the sample"), @@ -221,16 +231,23 @@ class AcquisitionModel(_AcquisitionModelFromDB): # type:ignore class ProcessModel(_ProcessModelFromDB): # type:ignore free_columns: Dict[str, Any] = Field(title="Free columns from process mapping in project", - default={}, example={"software":"zooprocess_pid_to_ecotaxa_7.26_2017/12/19", "pressure_gain":"10"}) + default={}, example={"software": "zooprocess_pid_to_ecotaxa_7.26_2017/12/19", + "pressure_gain": "10"}) class CreateProjectReq(BaseModel): - clone_of_id: int = Field(title="Clone of id", description="Internal, numeric id of a project to clone as a new one. By default it does not clone anything.", default=None, + clone_of_id: int = Field(title="Clone of id", + description="Internal, numeric id of a project to clone as a new one. By default it does not clone anything.", + default=None, example=2) title: str = Field(title="Title", description="The project title, as text.", example="My new project title") - visible: bool = Field(title="Visible", description="When TRUE, the project is created visible by all users.", default=True, + visible: bool = Field(title="Visible", description="When TRUE, the project is created visible by all users.", + default=True, example=True) + class Config: + schema_extra = {"title": "Create project request Model"} + class ProjectFilters(TypedDict, total=False): taxo: Optional[str] @@ -299,10 +316,15 @@ class ProjectFilters(TypedDict, total=False): """ Coma-separated list of annotator, i.e. person who validated the classification in last. """ -#TODO JCE - examples + +# TODO JCE - examples _DBProjectFilters = { - "taxo": Field(title="Taxo", description="Coma-separated list of numeric taxonomy/category ids. Only include objects classified with one of them", example="12,7654,5409"), - "taxochild": Field(title="Taxo child", description="If 'Y' and taxo is set, also include children of each member of 'taxo' list in taxonomy tree", example="Y"), + "taxo": Field(title="Taxo", + description="Coma-separated list of numeric taxonomy/category ids. Only include objects classified with one of them", + example="12,7654,5409"), + "taxochild": Field(title="Taxo child", + description="If 'Y' and taxo is set, also include children of each member of 'taxo' list in taxonomy tree", + example="Y"), "statusfilter": Field(title="", description="""Include objects with given status: 'NV': Not validated 'PV': Predicted or Validated @@ -312,37 +334,87 @@ class ProjectFilters(TypedDict, total=False): 'U': Not classified other: direct equality comparison with DB value """, example="NV"), - "MapN": Field(title="Map North", description="If all 4 are set (MapN, MapW, MapE, MapS), include objects inside the defined bounding rectangle.", example=44.34), - "MapW": Field(title="Map West", description="If all 4 are set (MapN, MapW, MapE, MapS), include objects inside the defined bounding rectangle.", example=3.88), - "MapE": Field(title="Map East", description="If all 4 are set (MapN, MapW, MapE, MapS), include objects inside the defined bounding rectangle.", example=7.94), - "MapS": Field(title="Map South", description="If all 4 are set (MapN, MapW, MapE, MapS), include objects inside the defined bounding rectangle.", example=42.42), - "depthmin": Field(title="Depthmin", description="Positive values. If both are set (depthmin, depthmax), include objects for which both depths (min and max) are inside the range", example="10"), - "depthmax": Field(title="Depthmax", description="Positive values. If both are set (depthmin, depthmax), include objects for which both depths (min and max) are inside the range", example="110"), - "samples": Field(title="Samples", description="Coma-separated list of sample IDs, include only objects for these samples", example="10987,3456,987,38"), - "instrum": Field(title="Instrument", description="Instrument name, include objects for which sampling was done using this instrument", example="uvp5"), - "daytime": Field(title="Day time", description="Coma-separated list of sun position values: D for Day, U for Dusk, N for Night, A for Dawn (Aube in French)", example="N,A"), + "MapN": Field(title="Map North", + description="If all 4 are set (MapN, MapW, MapE, MapS), include objects inside the defined bounding rectangle.", + example=44.34), + "MapW": Field(title="Map West", + description="If all 4 are set (MapN, MapW, MapE, MapS), include objects inside the defined bounding rectangle.", + example=3.88), + "MapE": Field(title="Map East", + description="If all 4 are set (MapN, MapW, MapE, MapS), include objects inside the defined bounding rectangle.", + example=7.94), + "MapS": Field(title="Map South", + description="If all 4 are set (MapN, MapW, MapE, MapS), include objects inside the defined bounding rectangle.", + example=42.42), + "depthmin": Field(title="Depthmin", + description="Positive values. If both are set (depthmin, depthmax), include objects for which both depths (min and max) are inside the range", + example="10"), + "depthmax": Field(title="Depthmax", + description="Positive values. If both are set (depthmin, depthmax), include objects for which both depths (min and max) are inside the range", + example="110"), + "samples": Field(title="Samples", + description="Coma-separated list of sample IDs, include only objects for these samples", + example="10987,3456,987,38"), + "instrum": Field(title="Instrument", + description="Instrument name, include objects for which sampling was done using this instrument", + example="uvp5"), + "daytime": Field(title="Day time", + description="Coma-separated list of sun position values: D for Day, U for Dusk, N for Night, A for Dawn (Aube in French)", + example="N,A"), "month": Field(title="Month", description="Coma-separated list of month numbers, 1=Jan and so on", example="11,12"), - "fromdate": Field(title="From date", description="Format is 'YYYY-MM-DD', include objects collected after this date", example="2020-10-09"), - "todate": Field(title="To date", description="Format is 'YYYY-MM-DD', include objects collected before this date", example="2021-10-09"), - "fromtime": Field(title="From time", description="Format is 'HH24:MM:SS', include objects collected after this time of day", example="1:17:00"), - "totime": Field(title="To time", description="Format is 'HH24:MM:SS', include objects collected before this time of day", example="23:32:00"), - "inverttime": Field(title="Invert time", description="If '1', include objects outside fromtime and totime range", example="0"), - "validfromdate": Field(title="Valid from date", description="Format is 'YYYY-MM-DD HH24:MI', include objects validated/set to dubious after this date+time", example="2020-10-09 10:00:00"), - "validtodate": Field(title="Valid to date", description="Format is 'YYYY-MM-DD HH24:MI', include objects validated/set to dubious before this date+time", example="2021-10-09 10:00:00"), - "freenum": Field(title="Free num", description="Numerical DB column number in Object as basis for the 2 following criteria (freenumst, freenumend)", example=""), - "freenumst": Field(title="Freenum start", description="Start of included range for the column defined by freenum, in which objects are included", example=""), - "freenumend": Field(title="Free num end", description="End of included range for the column defined by freenum, in which objects are included", example=""), + "fromdate": Field(title="From date", + description="Format is 'YYYY-MM-DD', include objects collected after this date", + example="2020-10-09"), + "todate": Field(title="To date", description="Format is 'YYYY-MM-DD', include objects collected before this date", + example="2021-10-09"), + "fromtime": Field(title="From time", + description="Format is 'HH24:MM:SS', include objects collected after this time of day", + example="1:17:00"), + "totime": Field(title="To time", + description="Format is 'HH24:MM:SS', include objects collected before this time of day", + example="23:32:00"), + "inverttime": Field(title="Invert time", description="If '1', include objects outside fromtime and totime range", + example="0"), + "validfromdate": Field(title="Valid from date", + description="Format is 'YYYY-MM-DD HH24:MI', include objects validated/set to dubious after this date+time", + example="2020-10-09 10:00:00"), + "validtodate": Field(title="Valid to date", + description="Format is 'YYYY-MM-DD HH24:MI', include objects validated/set to dubious before this date+time", + example="2021-10-09 10:00:00"), + "freenum": Field(title="Free num", + description="Numerical DB column number in Object as basis for the 2 following criteria (freenumst, freenumend)", + example=""), + "freenumst": Field(title="Freenum start", + description="Start of included range for the column defined by freenum, in which objects are included", + example=""), + "freenumend": Field(title="Free num end", + description="End of included range for the column defined by freenum, in which objects are included", + example=""), "freetxt": Field(title="Free text", description=""" Textual DB column number as basis for following criteria If starts with 's' then it's a text column in Sample If starts with 'a' then it's a text column in Acquisition If starts with 'p' then it's a text column in Process If starts with 'o' then it's a text column in Object """, example=""), - "freetxtval": Field(title="Free text val", description="Text to match in the column defined by freetxt, for an object to be include", example=""), - "filt_annot": Field(title="Filter annotator", description="Coma-separated list of annotator, i.e. person who validated the classification at any point in time. ", example=""), - "filt_last_annot": Field(title="Filter last annotator", description="Coma-separated list of annotator, i.e. person who validated the classification in last. ", example="") + "freetxtval": Field(title="Free text val", + description="Text to match in the column defined by freetxt, for an object to be include", + example=""), + "filt_annot": Field(title="Filter annotator", + description="Coma-separated list of annotator, i.e. person who validated the classification at any point in time. ", + example=""), + "filt_last_annot": Field(title="Filter last annotator", + description="Coma-separated list of annotator, i.e. person who validated the classification in last. ", + example="") } -ProjectFiltersModel = typed_dict_to_model(ProjectFilters,field_infos=_DBProjectFilters) + + +class ProjectFiltersModelConfig: + schema_extra = {"title": "Project filters Model", + "description": "How to reduce project data"} + + +ProjectFiltersModel = typed_dict_to_model(ProjectFilters, field_infos=_DBProjectFilters, + config=ProjectFiltersModelConfig) class BulkUpdateReq(BaseModel): @@ -354,7 +426,10 @@ class BulkUpdateReq(BaseModel): uval : The new value to set, always as a string \n\n \ }", example=[{"ucol": "sub_part", "uval": "2"}]) - # updates: List[ColUpdate] = Field(title="Updates", description="The updates, to do on all impacted entities") + + # updates: List[ColUpdate] = Field(title="Updates", description="The updates, to do on all impacted entities") + class Config: + schema_extra = {"title": "Update request Model"} # TODO: Derive from ProjectTaxoStats @@ -373,25 +448,26 @@ class ProjectTaxoStatsModel(BaseModel): example=1345) -_DBProjectUserStatsDescription={ - "projid" : Field(title="Project id", description="The project id"), - "annotators" : Field(title="Annotators", description="The users who ever decided on classification or state of objects"), - "activities" : Field(title="Activities", description="More details on annotators' activities") +_DBProjectUserStatsDescription = { + "projid": Field(title="Project id", description="The project id"), + "annotators": Field(title="Annotators", + description="The users who ever decided on classification or state of objects"), + "activities": Field(title="Activities", description="More details on annotators' activities") } ProjectUserStatsModel = dataclass_to_model(ProjectUserStats, add_suffix=True, field_infos=_DBProjectUserStatsDescription) -_DBProjectSetColumnStatDescription={ +_DBProjectSetColumnStatDescription = { "proj_ids": Field(title="Projects IDs", description="Projects IDs from the call"), - "columns": Field(title = "Columns", description="Column names from the call"), - "total": Field(title = "Total of rows", description="All rows regardless of emptiness"), - "counts": Field(title = "Counts", description="Counts of non-empty values, one per column"), - "variances": Field(title = "Variances", description="Variances of values, one per column") + "columns": Field(title="Columns", description="Column names from the call"), + "total": Field(title="Total of rows", description="All rows regardless of emptiness"), + "counts": Field(title="Counts", description="Counts of non-empty values, one per column"), + "variances": Field(title="Variances", description="Variances of values, one per column") } -ProjectSetColumnStatsModel = dataclass_to_model(ProjectSetColumnStats, add_suffix=True, - field_infos=_DBProjectSetColumnStatDescription) +ProjectSetColumnStatsModel = dataclass_to_model(ProjectSetColumnStats, add_suffix=True, + field_infos=_DBProjectSetColumnStatDescription) class CreateCollectionReq(BaseModel): @@ -400,6 +476,9 @@ class CreateCollectionReq(BaseModel): project_ids: List[int] = Field(title="Project ids", description="The list of composing project IDs", example=[1], min_items=1) + class Config: + schema_extra = {"title": "Create collection request Model"} + class _AddedToCollection(BaseModel): """ @@ -429,16 +508,25 @@ class CollectionModel(_CollectionModelFromDB, _AddedToCollection): # type:ignor Basic and computed information about the Collection """ + class Config: + schema_extra = {"title": "Collection Model"} + + _DBJobDescription = { - "id" : Field(title="id",description="Job unique identifier", example=47445), - "owner_id" : Field(title="owner_id",description="The user who created and thus owns the job ", example=1), - "type" : Field(title="type",description="The job type, e.g. import, export... ", example="Subset"), - "state" : Field(title="state",description="What the job is doing. Could be 'P' for Pending (Waiting for an execution thread), 'R' for Running (Being executed inside a thread), 'A' for Asking (Needing user information before resuming), 'E' for Error (Stopped with error), 'F' for Finished (Done).", example=DBJobStateEnum.Finished), - "step" : Field(title="step",description="Where in the workflow the job is ", example="null"), - "progress_pct" : Field(title="progress_pct",description="The progress percentage for UI ", example=100), - "progress_msg" : Field(title="progress_msg",description="The message for UI, short version ", example="Done"), - "creation_date" : Field(title="creation_date",description="The date of creation of the Job, as text formatted according to the ISO 8601 standard.", example="2021-09-28T08:43:20.196061"), - "updated_on" : Field(title="updated_on",description="Last time that anything changed in present line ", example="2021-09-28T08:43:21.441969") + "id": Field(title="id", description="Job unique identifier", example=47445), + "owner_id": Field(title="owner_id", description="The user who created and thus owns the job ", example=1), + "type": Field(title="type", description="The job type, e.g. import, export... ", example="Subset"), + "state": Field(title="state", + description="What the job is doing. Could be 'P' for Pending (Waiting for an execution thread), 'R' for Running (Being executed inside a thread), 'A' for Asking (Needing user information before resuming), 'E' for Error (Stopped with error), 'F' for Finished (Done).", + example=DBJobStateEnum.Finished), + "step": Field(title="step", description="Where in the workflow the job is ", example="null"), + "progress_pct": Field(title="progress_pct", description="The progress percentage for UI ", example=100), + "progress_msg": Field(title="progress_msg", description="The message for UI, short version ", example="Done"), + "creation_date": Field(title="creation_date", + description="The date of creation of the Job, as text formatted according to the ISO 8601 standard.", + example="2021-09-28T08:43:20.196061"), + "updated_on": Field(title="updated_on", description="Last time that anything changed in present line ", + example="2021-09-28T08:43:21.441969") } _JobModelFromDB = sqlalchemy_to_pydantic(Job, exclude=[Job.params.name, Job.result.name, @@ -446,18 +534,30 @@ class CollectionModel(_CollectionModelFromDB, _AddedToCollection): # type:ignor Job.question.name, Job.reply.name, Job.inside.name], - field_infos=_DBJobDescription) + field_infos=_DBJobDescription) class _AddedToJob(BaseModel): """ What's added to a Job compared to the plain DB record. """ - params: Dict[str, Any] = Field(title="params", description="Creation parameters", default={}, example={"prj_id":1,"req":{"filters":{"taxo":"85067","taxochild":"N"},"dest_prj_id":1,"group_type":"S","limit_type":"P","limit_value":100.0,"do_images":True}}) - result: Dict[str, Any] = Field(title="result", description="Final result of the run", default={}, example={"rowcount":3}) + params: Dict[str, Any] = Field(title="params", description="Creation parameters", default={}, example={"prj_id": 1, + "req": { + "filters": { + "taxo": "85067", + "taxochild": "N"}, + "dest_prj_id": 1, + "group_type": "S", + "limit_type": "P", + "limit_value": 100.0, + "do_images": True}}) + result: Dict[str, Any] = Field(title="result", description="Final result of the run", default={}, + example={"rowcount": 3}) errors: List[str] = Field(title="errors", description="The errors seen during last step", default=[], example=[]) - question: Dict[str, Any] = Field(title="question", description="The data provoking job move to Asking state", default={}, example={}) - reply: Dict[str, Any] = Field(title="reply", description="The data provided as a reply to the question", default={}, example={}) + question: Dict[str, Any] = Field(title="question", description="The data provoking job move to Asking state", + default={}, example={}) + reply: Dict[str, Any] = Field(title="reply", description="The data provided as a reply to the question", default={}, + example={}) inside: Dict[str, Any] = Field(title="inside", description="Internal state of the job", default={}, example={}) diff --git a/py/API_models/exports.py b/py/API_models/exports.py index 628096dc..b0c4e2b0 100644 --- a/py/API_models/exports.py +++ b/py/API_models/exports.py @@ -57,6 +57,8 @@ class ExportReq(BaseModel): "Per A(cquisition) or S(ample) or ''") out_to_ftp: bool = Field(title="Copy result file to FTP area. Original file is still available.") + class Config: + schema_extra = {"title": "Export request Model"} class ExportRsp(EMODnetExportRsp): """ diff --git a/py/API_models/helpers/TypedDictToModel.py b/py/API_models/helpers/TypedDictToModel.py index fb45076f..9b1267b7 100644 --- a/py/API_models/helpers/TypedDictToModel.py +++ b/py/API_models/helpers/TypedDictToModel.py @@ -13,16 +13,17 @@ # noinspection PyPackageRequirements from pydantic import create_model - -from API_models.helpers import PydanticModelT # noinspection PyPackageRequirements from pydantic.fields import ModelField +from API_models.helpers import PydanticModelT + # Generify the def with input type T = TypeVar('T') -def typed_dict_to_model(typed_dict: T, field_infos: Optional[Dict[str, Any]] = None): # TODO -> Type[BaseModel]: +def typed_dict_to_model(typed_dict: T, field_infos: Optional[Dict[str, Any]] = None, + config: Any = None): # TODO -> Type[BaseModel], and type for config: annotations = {} for name, field in typed_dict.__annotations__.items(): if field == Optional[str]: @@ -32,7 +33,7 @@ def typed_dict_to_model(typed_dict: T, field_infos: Optional[Dict[str, Any]] = raise Exception("Not managed yet") # pragma:nocover ret: PydanticModelT = create_model( - typed_dict.__name__, **annotations # type: ignore + typed_dict.__name__, __config__=config, **annotations # type: ignore ) # Make the model get-able # noinspection PyTypeHints diff --git a/py/API_models/imports.py b/py/API_models/imports.py index c01ad76b..69ef16e4 100644 --- a/py/API_models/imports.py +++ b/py/API_models/imports.py @@ -20,6 +20,9 @@ class ImportReq(BaseModel): update_mode: str = Field(title="Update data ('Yes'), including classification ('Cla')", default="") + class Config: + schema_extra = {"title": "Import request Model"} + class ImportRsp(BaseModel): """ Import response. """ @@ -84,6 +87,9 @@ class SimpleImportReq(BaseModel): # default=[v for v in PossibleSimpleImportFields.__members__]) possible_values: List[str] = [v for v in SimpleImportFields.__members__] + class Config: + schema_extra = {"title": "Simple import request Model"} + class SimpleImportRsp(BaseModel): """ Simple Import, response. """ diff --git a/py/API_models/login.py b/py/API_models/login.py index 755e2548..b1f9db85 100644 --- a/py/API_models/login.py +++ b/py/API_models/login.py @@ -9,6 +9,9 @@ class LoginReq(BaseModel): - password: str = Field(title="User's password" , default=None, description="User password", example="test!") - username: str = Field(title="User's eamil", default=None, description="User email used during registration", example="ecotaxa.api.user@gmail.com") + password: str = Field(title="User's password", default=None, description="User password", example="test!") + username: str = Field(title="User's email", default=None, description="User email used during registration", + example="ecotaxa.api.user@gmail.com") + class Config: + schema_extra = {"title": "Login request Model"} diff --git a/py/API_models/objects.py b/py/API_models/objects.py index 9922bfd5..ee43ddd3 100644 --- a/py/API_models/objects.py +++ b/py/API_models/objects.py @@ -15,31 +15,41 @@ from DB import Image, ObjectHeader from .helpers.pydantic import ResponseModel - -#TODO JCE - examples - ?default? +# TODO JCE - examples - ?default? _DBObjectHeaderDescription = { "objid": Field(title="Object Id", description="The object Id.", example=264409236), "acquisid": Field(title="Acquisition Id", description="The parent acquisition Id.", example=144), - "orig_id": Field(title="Original id", description="Original object ID from initial TSV load", example="deex_leg1_48_406"), - "objdate" : Field(title="Object date", description=""), - "objtime" : Field(title="Object time", description=""), + "orig_id": Field(title="Original id", description="Original object ID from initial TSV load", + example="deex_leg1_48_406"), + "objdate": Field(title="Object date", description=""), + "objtime": Field(title="Object time", description=""), "latitude": Field(title="Latitude", description="The latitude", example=42.0231666666667), "longitude": Field(title="Longitude", description="The longitude", example=4.71766666666667), - "depth_min" : Field(title="Depth min", description="The min depth", example=0), - "depth_max" : Field(title="Depth max", description="The min depth", example=300), - "sunpos" : Field(title="Sun position", description="Sun position, from date, time and coords", example="N"), + "depth_min": Field(title="Depth min", description="The min depth", example=0), + "depth_max": Field(title="Depth max", description="The min depth", example=300), + "sunpos": Field(title="Sun position", description="Sun position, from date, time and coords", example="N"), "classif_id": Field(title="Classification Id", description="The classification Id.", example=82399), - "classif_qual": Field(title="Classification qualification", description="The classification qualification. Could be **P** for predicted, **V** for validated or **D** for Dubious.", example="P"), - "classif_who": Field(title="Classification who", description="The user who manualy classify this object.", example="null"), - "classif_when" : Field(title="Classification when", description="The classification date.", example="2021-09-21T14:59:01.007110"), - "classif_auto_id" : Field(title="Classification auto Id", description="Set if the object was ever predicted, remain forever with these value. Reflect the 'last state' only if classif_qual is 'P'. "), - "classif_auto_score" : Field(title="Classification auto score", description="Set if the object was ever predicted, remain forever with these value. Reflect the 'last state' only if classif_qual is 'P'. The classification auto score is generally between 0 and 1. This is a confidence score, in the fact that, the taxon prediction for this object is correct.", example=0.085), - "classif_auto_when" : Field(title="Classification auto when", description="Set if the object was ever predicted, remain forever with these value. Reflect the 'last state' only if classif_qual is 'P'. The classification date.", example="2021-09-21T14:59:01.007110"), - "classif_crossvalidation_id" : Field(title="Classification crossvalidation Id", description="Always NULL in prod", example="null"), - "complement_info" : Field(title="Complement info", description="", example="Part of ostracoda"), - "similarity" : Field(title="Similarity", description="Always NULL in prod", example="null"), - "random_value" : Field(title="random_value", description=""), - "object_link" : Field(title="Object link", description="Object link", example="http://www.zooscan.obs-vlfr.fr//") + "classif_qual": Field(title="Classification qualification", + description="The classification qualification. Could be **P** for predicted, **V** for validated or **D** for Dubious.", + example="P"), + "classif_who": Field(title="Classification who", description="The user who manualy classify this object.", + example="null"), + "classif_when": Field(title="Classification when", description="The classification date.", + example="2021-09-21T14:59:01.007110"), + "classif_auto_id": Field(title="Classification auto Id", + description="Set if the object was ever predicted, remain forever with these value. Reflect the 'last state' only if classif_qual is 'P'. "), + "classif_auto_score": Field(title="Classification auto score", + description="Set if the object was ever predicted, remain forever with these value. Reflect the 'last state' only if classif_qual is 'P'. The classification auto score is generally between 0 and 1. This is a confidence score, in the fact that, the taxon prediction for this object is correct.", + example=0.085), + "classif_auto_when": Field(title="Classification auto when", + description="Set if the object was ever predicted, remain forever with these value. Reflect the 'last state' only if classif_qual is 'P'. The classification date.", + example="2021-09-21T14:59:01.007110"), + "classif_crossvalidation_id": Field(title="Classification crossvalidation Id", description="Always NULL in prod", + example="null"), + "complement_info": Field(title="Complement info", description="", example="Part of ostracoda"), + "similarity": Field(title="Similarity", description="Always NULL in prod", example="null"), + "random_value": Field(title="random_value", description=""), + "object_link": Field(title="Object link", description="Object link", example="http://www.zooscan.obs-vlfr.fr//") } ObjectHeaderModel = sqlalchemy_to_pydantic(ObjectHeader, field_infos=_DBObjectHeaderDescription) @@ -48,18 +58,19 @@ class ObjectFieldsModel(BaseModel): pass -#TODO JCE - descriptions + +# TODO JCE - descriptions _DBImageDescription = { - "imgid":Field(title="Image Id", description="The id of the image", example=376456), - "objid":Field(title="Object Id", description="The id of the object related to the image", example=376456), - "imgrank":Field(title="Image rank", description="", example=0), - "file_name":Field(title="File name", description="", example="0037/6456.jpg"), - "orig_file_name":Field(title="Original file name", description="", example="dewex_leg2_63_689.jpg"), - "width":Field(title="Width", description="", example=98), - "height":Field(title="Height", description="", example=63), - "thumb_file_name":Field(title="Thumb file name", description="", example="null"), - "thumb_width":Field(title="Thumb width", description="", example="null"), - "thumb_height":Field(title="Thumb height", description="", example="null") + "imgid": Field(title="Image Id", description="The id of the image", example=376456), + "objid": Field(title="Object Id", description="The id of the object related to the image", example=376456), + "imgrank": Field(title="Image rank", description="", example=0), + "file_name": Field(title="File name", description="", example="0037/6456.jpg"), + "orig_file_name": Field(title="Original file name", description="", example="dewex_leg2_63_689.jpg"), + "width": Field(title="Width", description="", example=98), + "height": Field(title="Height", description="", example=63), + "thumb_file_name": Field(title="Thumb file name", description="", example="null"), + "thumb_width": Field(title="Thumb width", description="", example="null"), + "thumb_height": Field(title="Thumb height", description="", example="null") } _ImageModelFromDB = sqlalchemy_to_pydantic(Image, field_infos=_DBImageDescription) @@ -70,28 +81,44 @@ class ImageModel(_ImageModelFromDB): # type:ignore class ObjectModel(ObjectHeaderModel, ObjectFieldsModel): # type:ignore - orig_id: str = Field(title="Original id", description="Original object ID from initial TSV load", example="deex_leg1_48_406") - object_link: Optional[str] = Field(title="Object link", description="Object link", example="http://www.zooscan.obs-vlfr.fr//") + orig_id: str = Field(title="Original id", description="Original object ID from initial TSV load", + example="deex_leg1_48_406") + object_link: Optional[str] = Field(title="Object link", description="Object link", + example="http://www.zooscan.obs-vlfr.fr//") sample_id: int = Field(title="Sample id", description="Sample (i.e. parent of parent acquisition) ID", example=12) project_id: int = Field(title="Project id", description="Project (i.e. parent of sample) ID", example=76) - images: List[ImageModel] = Field(title="Images", description="Images for this object", default=[]) - free_columns: Dict[str, Any] = Field(title="Free columns", description="Free columns from object mapping in project", example={"area":49.0,"mean":232.27,"stddev":2.129}, default={}) + images: List[ImageModel] = Field(title="Images", description="Images for this object", default=[]) + free_columns: Dict[str, Any] = Field(title="Free columns", + description="Free columns from object mapping in project", + example={"area": 49.0, "mean": 232.27, "stddev": 2.129}, default={}) class ObjectSetQueryRsp(ResponseModel): - object_ids: ObjectIDListT = Field(title="Object Ids", description="Matching object IDs", default=[], example=[634509, 6234516, 976544]) - acquisition_ids: List[Optional[int]] = Field(title="Acquisition Ids", description="Parent (acquisition) IDs", default=[], example=[23,987,89]) - sample_ids: List[Optional[int]] = Field(title="Sample Ids", description="Parent (sample) IDs", default=[], example=[234,194,12]) - project_ids: List[Optional[int]] = Field(title="Project Ids", description="Project Ids", default=[], example=[22,43]) - details: List[List] = Field(title="Details", description="Requested fields, in request order", default=[])#TODO JCE - total_ids: int = Field(title="Total Ids", description="Total rows returned by the query, even if it was window-ed", default=0, example=1000) + object_ids: ObjectIDListT = Field(title="Object Ids", description="Matching object IDs", default=[], + example=[634509, 6234516, 976544]) + acquisition_ids: List[Optional[int]] = Field(title="Acquisition Ids", description="Parent (acquisition) IDs", + default=[], example=[23, 987, 89]) + sample_ids: List[Optional[int]] = Field(title="Sample Ids", description="Parent (sample) IDs", default=[], + example=[234, 194, 12]) + project_ids: List[Optional[int]] = Field(title="Project Ids", description="Project Ids", default=[], + example=[22, 43]) + details: List[List] = Field(title="Details", description="Requested fields, in request order", + default=[]) # TODO JCE + total_ids: int = Field(title="Total Ids", description="Total rows returned by the query, even if it was window-ed", + default=0, example=1000) class ObjectSetSummaryRsp(ResponseModel): - total_objects: Optional[int] = Field(title="Total objects", description="Total number of objects in the set", default=None, example=400) - validated_objects: Optional[int] = Field(title="Validated objects", description="Number of validated objects in the set", default=None, example=100) - dubious_objects: Optional[int] = Field(title="Dubious objects", description="Number of dubious objects in the set", default=None, example=100) - predicted_objects: Optional[int] = Field(title="Predicted objects", description="Number of predicted objects in the set", default=None, example=100) + total_objects: Optional[int] = Field(title="Total objects", description="Total number of objects in the set", + default=None, example=400) + validated_objects: Optional[int] = Field(title="Validated objects", + description="Number of validated objects in the set", default=None, + example=100) + dubious_objects: Optional[int] = Field(title="Dubious objects", description="Number of dubious objects in the set", + default=None, example=100) + predicted_objects: Optional[int] = Field(title="Predicted objects", + description="Number of predicted objects in the set", default=None, + example=100) HistoricalLastClassificationModel = dataclass_to_model(HistoricalLastClassif) @@ -99,28 +126,41 @@ class ObjectSetSummaryRsp(ResponseModel): class ObjectSetRevertToHistoryRsp(BaseModel): # TODO: Setting below to List[HistoricalClassification] fails to export the model - # but setting as below fools mypy. - last_entries: List[HistoricalLastClassificationModel] = Field(title="Last entries", description="Object + last classification", # type: ignore + # but setting as below fools mypy... or not + last_entries: List[HistoricalLastClassificationModel] = Field(title="Last entries", # type:ignore + description="Object + last classification", default=[]) # TODO: Below is ClassifSetInfoT but this defeats openapi generator - classif_info: Dict[int, Any] = Field(title="Classification info", description="Classification names (self+parent) for involved IDs", + classif_info: Dict[int, Any] = Field(title="Classification info", + description="Classification names (self+parent) for involved IDs", default={}) -#TODO JCE - examples +# TODO JCE - examples class ClassifyReq(BaseModel): - target_ids: List[int] = Field(title="Target Ids", description="The IDs of the target objects", example=[634509, 6234516, 976544]) - classifications: List[int] = Field(title="Classifications", description="The wanted new classifications, i.e. taxon ID, one for each object. Use -1 to keep present one.", example=[7546, 3421, 788]) - wanted_qualification: str = Field(title="Wanted qualification", description="The wanted qualifications for all objects. 'V' and 'P'.") + target_ids: List[int] = Field(title="Target Ids", description="The IDs of the target objects", + example=[634509, 6234516, 976544]) + classifications: List[int] = Field(title="Classifications", + description="The wanted new classifications, i.e. taxon ID, one for each object. Use -1 to keep present one.", + example=[7546, 3421, 788]) + wanted_qualification: str = Field(title="Wanted qualification", + description="The wanted qualifications for all objects. 'V' and 'P'.") + + class Config: + schema_extra = {"title": "Classify request Model"} class ClassifyAutoReq(BaseModel): target_ids: List[int] = Field(title="Target Ids", description="The IDs of the target objects") - classifications: List[int] = Field(title="Classifications", description="The wanted new classifications, i.e. taxon ID, one for each object.") - scores: List[float] = Field(title="Scores", description="The classification score is generally between 0 and 1. It indicates the probability that the taxon prediction of this object is correct.") + classifications: List[int] = Field(title="Classifications", + description="The wanted new classifications, i.e. taxon ID, one for each object.") + scores: List[float] = Field(title="Scores", + description="The classification score is generally between 0 and 1. It indicates the probability that the taxon prediction of this object is correct.") keep_log: bool = Field(title="Keep log", description="Set if former automatic classification history is needed.") + class Config: schema_extra = { + "title": "Classify auto request Model", "example": { "target_ids": [634509, 6234516, 976544], "classifications": [7546, 3421, 788], @@ -129,19 +169,30 @@ class Config: } } -#TODO JCE - description + +# TODO JCE - description _DBHistoricalClassificationDescription = { "objid": Field(title="Object Id", description="The object Id.", example=264409236), "classif_id": Field(title="Classification Id", description="The classification Id.", example=82399), - "classif_date": Field(title="Classification date", description="The classification date.", example="2021-09-21T14:59:01.007110"), - "classif_who": Field(title="Classification who", description="The user who manualy classify this object.", example="null"), - "classif_type": Field(title="Classification type", description="The type of classification. Could be **A** for Automatic or **M** for Manual.", example="A"), - "classif_qual": Field(title="Classification qualification", description="The classification qualification. Could be **P** for predicted, **V** for validated or **D** for Dubious.", example="P"), - "classif_score": Field(title="Classification score", description="The classification score is generally between 0 and 1. This is a confidence score, in the fact that, the taxon prediction for this object is correct.", example=0.085), - "user_name": Field(title="User name", description="The name of the user who classified this object.", example="null"), + "classif_date": Field(title="Classification date", description="The classification date.", + example="2021-09-21T14:59:01.007110"), + "classif_who": Field(title="Classification who", description="The user who manualy classify this object.", + example="null"), + "classif_type": Field(title="Classification type", + description="The type of classification. Could be **A** for Automatic or **M** for Manual.", + example="A"), + "classif_qual": Field(title="Classification qualification", + description="The classification qualification. Could be **P** for predicted, **V** for validated or **D** for Dubious.", + example="P"), + "classif_score": Field(title="Classification score", + description="The classification score is generally between 0 and 1. This is a confidence score, in the fact that, the taxon prediction for this object is correct.", + example=0.085), + "user_name": Field(title="User name", description="The name of the user who classified this object.", + example="null"), "taxon_name": Field(title="Taxon name", description="The taxon name of the object.", example="Penilia avirostris") } -HistoricalClassificationModel = dataclass_to_model(HistoricalClassification, field_infos=_DBHistoricalClassificationDescription) +HistoricalClassificationModel = dataclass_to_model(HistoricalClassification, + field_infos=_DBHistoricalClassificationDescription) class ObjectHistoryRsp(ResponseModel): diff --git a/py/API_models/prediction.py b/py/API_models/prediction.py index f5b652c0..702dc32a 100644 --- a/py/API_models/prediction.py +++ b/py/API_models/prediction.py @@ -11,20 +11,27 @@ class PredictionReq(BaseModel): """ Prediction, AKA Auto Classification, request. """ - project_id: int = Field(title="Project Id", description="The destination project, of which objects will be predicted.") - source_project_ids: List[int] = Field(title="Source project Ids", description="The source projects, objects in them will serve as reference.", - min_items=1) - features: List[str] = Field(title="Features", description="The object features AKA free column, to use in the algorithm. " - "Features must be common to all projects, source ones and destination one", + project_id: int = Field(title="Project Id", + description="The destination project, of which objects will be predicted.") + source_project_ids: List[int] = Field(title="Source project Ids", + description="The source projects, objects in them will serve as reference.", + min_items=1) + features: List[str] = Field(title="Features", + description="The object features AKA free column, to use in the algorithm. " + "Features must be common to all projects, source ones and destination one", min_items=1) - categories: List[int] = Field(title="Categories", description="In source projects, only objects validated with these categories " - "will be considered.", + categories: List[int] = Field(title="Categories", + description="In source projects, only objects validated with these categories " + "will be considered.", min_items=1) - use_scn: bool = Field(title="Use scn", description="Use extra features, generated using the image, for improving the prediction.", + use_scn: bool = Field(title="Use scn", + description="Use extra features, generated using the image, for improving the prediction.", default=False) class Config: schema_extra = { + "title": "Prediction Request", + "description": "How to predict, in details.", "example": { "project_id": [3426], "source_project_ids": [1040, 1820], @@ -38,7 +45,8 @@ class PredictionRsp(BaseModel): """ Prediction response. """ - errors: List[str] = Field(title="Errors", description="Showstopper problems found while preparing the prediction.", + errors: List[str] = Field(title="Errors", + description="Showstopper problems found while preparing the prediction.", example=[], default=[]) warnings: List[str] = Field(title="Warnings", description="Problems found while preparing the prediction.", example=[], default=[]) diff --git a/py/API_models/subset.py b/py/API_models/subset.py index a89630cf..1a5275b0 100644 --- a/py/API_models/subset.py +++ b/py/API_models/subset.py @@ -28,6 +28,9 @@ class SubsetReq(BaseModel): limit_value: float = Field(title="Limit value", description="Limit value, e.g. 20% or 5 per copepoda or 5% per sample.", example=10.0) do_images: bool = Field(title="Do images", description="If set, also clone images.", example=True) + class Config: + schema_extra = {"title": "Subset request Model"} + class SubsetRsp(BaseModel): """ Subset response. """ diff --git a/py/main.py b/py/main.py index 8b387bb0..0737a74c 100644 --- a/py/main.py +++ b/py/main.py @@ -8,7 +8,8 @@ from logging import INFO from typing import Union, Tuple -from fastapi import FastAPI, Request, Response, status, Depends, HTTPException, UploadFile, File, Query, Form, Body, Path +from fastapi import FastAPI, Request, Response, status, Depends, HTTPException, UploadFile, File, Query, Form, Body, \ + Path from fastapi.logger import logger as fastapi_logger from fastapi.responses import StreamingResponse, FileResponse from fastapi.templating import Jinja2Templates @@ -83,7 +84,7 @@ fastapi_logger.setLevel(INFO) app = FastAPI(title="EcoTaxa", - version="0.0.18", + version="0.0.19", # openapi URL as seen from navigator, this is included when /docs is required # which serves swagger-ui JS app. Stay in /api sub-path. openapi_url="/api/openapi.json", @@ -130,7 +131,7 @@ }, response_model=str ) -async def login(params: LoginReq=Body(title="Login request Model", default=None)) -> str: +async def login(params: LoginReq = Body(...)) -> str: """ **Login barrier,** @@ -181,10 +182,11 @@ def show_current_user(current_user: int = Depends(get_current_user)): } }, response_model=str) -def get_current_user_prefs(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - key: str = Query(default=None, title="Key", description="The preference key, as text.", - example="filters"), - current_user: int = Depends(get_current_user)) -> str: +def get_current_user_prefs( + project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + key: str = Query(default=None, title="Key", description="The preference key, as text.", + example="filters"), + current_user: int = Depends(get_current_user)) -> str: """ **Returns one preference**, for a project and the currently authenticated user. @@ -204,13 +206,14 @@ def get_current_user_prefs(project_id: int = Path(description="Internal, numeric } } }) -def set_current_user_prefs(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - key: str = Query(default=None, title="Key", description="The preference key, as text.", - example="filters"), - value: str = Query(default=None, title="Value", - description="The value to set this preference to, as text.", - example="{\"dispfield\": \" dispfield_orig_id dispfield_classif_auto_score dispfield_classif_when dispfield_random_value\", \"ipp\": \"500\", \"magenabled\": \"1\", \"popupenabled\": \"1\", \"sortby\": \"orig_id\", \"sortorder\": \"asc\", \"statusfilter\": \"\", \"zoom\": \"90\"}"), - current_user: int = Depends(get_current_user)): +def set_current_user_prefs( + project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + key: str = Query(default=None, title="Key", description="The preference key, as text.", + example="filters"), + value: str = Query(default=None, title="Value", + description="The value to set this preference to, as text.", + example="{\"dispfield\": \" dispfield_orig_id dispfield_classif_auto_score dispfield_classif_when dispfield_random_value\", \"ipp\": \"500\", \"magenabled\": \"1\", \"popupenabled\": \"1\", \"sortby\": \"orig_id\", \"sortorder\": \"asc\", \"statusfilter\": \"\", \"zoom\": \"90\"}"), + current_user: int = Depends(get_current_user)): """ **Sets one preference**, for a project and for the currently authenticated user. @@ -264,7 +267,7 @@ def get_user(user_id: int = Path(description="Internal, the unique numeric id of } }, response_model=int) -def create_collection(params: CreateCollectionReq=Body(title="Create collection request Model", default=None), +def create_collection(params: CreateCollectionReq = Body(...), current_user: int = Depends(get_current_user)) -> Union[int, str]: """ **Create a collection** with at least one project inside. @@ -332,8 +335,10 @@ def collection_by_short_title( @app.get("/collections/{collection_id}", tags=['collections'], response_model=CollectionModel) -def get_collection(collection_id: int = Path(description="Internal, the unique numeric id of this collection.", default=None, example=1), - current_user: int = Depends(get_current_user)): +def get_collection( + collection_id: int = Path(description="Internal, the unique numeric id of this collection.", default=None, + example=1), + current_user: int = Depends(get_current_user)): """ Returns **information about the collection** corresponding to the given id. @@ -357,8 +362,9 @@ def get_collection(collection_id: int = Path(description="Internal, the unique n } } }) -def update_collection(collection: CollectionModel = Body(title="Collection Model", default=None), - collection_id: int = Path(description="Internal, the unique numeric id of this collection.", default=None, example=1), +def update_collection(collection: CollectionModel = Body(...), + collection_id: int = Path(description="Internal, the unique numeric id of this collection.", + default=None, example=1), current_user: int = Depends(get_current_user)): """ **Update the collection**. Note that some updates are silently failing when not compatible @@ -387,20 +393,22 @@ def update_collection(collection: CollectionModel = Body(title="Collection Model @app.get("/collections/{collection_id}/export/emodnet", tags=['collections'], response_model=EMODnetExportRsp) -def emodnet_format_export(collection_id: int = Path(description="Internal, the unique numeric id of this collection.", default=None, example=1), - dry_run: bool = Query(default=None, title="Dry run", - description="If set, then only a diagnostic of doability will be done.", - example=False), - with_zeroes: bool = Query(default=None, title="With zeroes", - description="If set, then *absent* records will be generated, in the relevant samples, for categories present in other samples.", - example=False), - auto_morpho: bool = Query(default=None, title="Auto morpho", - description="If set, then any object classified on a Morpho category will be added to the count of the nearest Phylo parent, upward in the tree.", - example=False), - with_computations: bool = Query(default=None, title="With computations", - description="If set, then an attempt will be made to compute organisms concentrations and biovolumes.", - example=False), - current_user: int = Depends(get_current_user)) -> EMODnetExportRsp: +def emodnet_format_export( + collection_id: int = Path(description="Internal, the unique numeric id of this collection.", default=None, + example=1), + dry_run: bool = Query(default=None, title="Dry run", + description="If set, then only a diagnostic of doability will be done.", + example=False), + with_zeroes: bool = Query(default=None, title="With zeroes", + description="If set, then *absent* records will be generated, in the relevant samples, for categories present in other samples.", + example=False), + auto_morpho: bool = Query(default=None, title="Auto morpho", + description="If set, then any object classified on a Morpho category will be added to the count of the nearest Phylo parent, upward in the tree.", + example=False), + with_computations: bool = Query(default=None, title="With computations", + description="If set, then an attempt will be made to compute organisms concentrations and biovolumes.", + example=False), + current_user: int = Depends(get_current_user)) -> EMODnetExportRsp: """ **Export the collection in EMODnet format**, @see https://www.emodnet-ingestion.eu @@ -426,8 +434,10 @@ def emodnet_format_export(collection_id: int = Path(description="Internal, the u } }, response_model=int) -def erase_collection(collection_id: int = Path(description="Internal, the unique numeric id of this collection.", default=None, example=1), - current_user: int = Depends(get_current_user)) -> int: +def erase_collection( + collection_id: int = Path(description="Internal, the unique numeric id of this collection.", default=None, + example=1), + current_user: int = Depends(get_current_user)) -> int: """ **Delete the collection**, @@ -503,7 +513,7 @@ def search_projects(current_user: Optional[int] = Depends(get_optional_current_u } }, response_model=int) -def create_project(params: CreateProjectReq = Body(title="Create project request Model", default=None), +def create_project(params: CreateProjectReq = Body(...), current_user: int = Depends(get_current_user)) -> Union[int, str]: """ **Create an empty project with only a title,** and **return the numeric id of this newly created project**. @@ -522,8 +532,8 @@ def create_project(params: CreateProjectReq = Body(title="Create project reques @app.post("/projects/{project_id}/subset", tags=['projects'], response_model=SubsetRsp) -def project_subset(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - params: SubsetReq = Body(title="Subset request Model", default=None), +def project_subset(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + params: SubsetReq = Body(...), current_user: int = Depends(get_current_user)): """ **Subset a project into another one.** @@ -648,7 +658,7 @@ def project_set_get_column_stats(ids: str = Query(title="Project ids", @app.post("/projects/{project_id}/dump", tags=['projects'], include_in_schema=False) # pragma:nocover def project_dump(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - filters: ProjectFiltersModel = Body(title="Project filters Model", default=None), + filters: ProjectFiltersModel = Body(...), current_user: int = Depends(get_current_user)): """ Dump the project in JSON form. Internal so far. @@ -728,8 +738,9 @@ def project_stats(project_id: int = Path(description="Internal, numeric id of th } } }) -def project_recompute_geography(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - current_user: int = Depends(get_current_user)) -> None: +def project_recompute_geography( + project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + current_user: int = Depends(get_current_user)) -> None: """ **Recompute geography information** for all samples in project. @@ -744,7 +755,7 @@ def project_recompute_geography(project_id: int = Path(description="Internal, nu @app.post("/file_import/{project_id}", tags=['projects'], response_model=ImportRsp) def import_file(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - params: ImportReq = Body(title="Import request Model", default=None), + params: ImportReq = Body(...), current_user: int = Depends(get_current_user)): """ Validate or do a real import of an EcoTaxa archive or directory. @@ -757,7 +768,7 @@ def import_file(project_id: int = Path(description="Internal, numeric id of the @app.post("/simple_import/{project_id}", tags=['projects'], response_model=SimpleImportRsp) def simple_import(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - params: SimpleImportReq = Body(title="Simple import request Model", default=None), + params: SimpleImportReq = Body(...), dry_run: bool = Query(title="Dry run", description="If set, then only a diagnostic of doability will be done. In this case, plain value check. If no dry_run, this call will create a background job.", default=None, example=True), @@ -904,7 +915,7 @@ def sample_set_get_stats(sample_ids: str = Query(default=None, title="Sample Ids } }, response_model=int) -def update_samples(req: BulkUpdateReq = Body(title="Update request Model", default=None), +def update_samples(req: BulkUpdateReq = Body(...), current_user: int = Depends(get_current_user)) -> int: """ Do the required **update for each sample in the set.** @@ -919,8 +930,9 @@ def update_samples(req: BulkUpdateReq = Body(title="Update request Model", defau @app.get("/sample/{sample_id}", tags=['samples'], response_model=SampleModel) -def sample_query(sample_id: int = Path(description="Internal, the unique numeric id of this sample.", default=None, example=1), - current_user: Optional[int] = Depends(get_optional_current_user)) \ +def sample_query( + sample_id: int = Path(description="Internal, the unique numeric id of this sample.", default=None, example=1), + current_user: Optional[int] = Depends(get_optional_current_user)) \ -> SampleBO: """ Returns **information about the sample** corresponding to the given id. @@ -961,7 +973,7 @@ def acquisitions_search( } }, response_model=int) -def update_acquisitions(req: BulkUpdateReq = Body(title="Update request Model", default=None), +def update_acquisitions(req: BulkUpdateReq = Body(...), current_user: int = Depends(get_current_user)) -> int: """ Do the required **update for each acquisition in the set**. @@ -974,8 +986,10 @@ def update_acquisitions(req: BulkUpdateReq = Body(title="Update request Model", @app.get("/acquisition/{acquisition_id}", tags=['acquisitions'], response_model=AcquisitionModel) -def acquisition_query(acquisition_id: int = Path(description="Internal, the unique numeric id of this acquisition.", default=None, example=1), - current_user: Optional[int] = Depends(get_optional_current_user)) \ +def acquisition_query( + acquisition_id: int = Path(description="Internal, the unique numeric id of this acquisition.", default=None, + example=1), + current_user: Optional[int] = Depends(get_optional_current_user)) \ -> AcquisitionBO: """ Returns **information about the acquisition** corresponding to the given id. @@ -1022,8 +1036,8 @@ def instrument_query(project_ids: str = Query(title="Projects ids", # ######################## END OF INSTRUMENT -@app.post("/process_set/update", tags=['processes'], -responses={ +@app.post("/process_set/update", tags=['processes'], + responses={ 200: { "content": { "application/json": { @@ -1033,8 +1047,8 @@ def instrument_query(project_ids: str = Query(title="Projects ids", } }, response_model=int -) -def update_processes(req: BulkUpdateReq = Body(title="Update request Model", default=None), + ) +def update_processes(req: BulkUpdateReq = Body(...), current_user: int = Depends(get_current_user)) -> int: """ Do the required **update for each process in the set.** @@ -1047,8 +1061,9 @@ def update_processes(req: BulkUpdateReq = Body(title="Update request Model", def @app.get("/process/{process_id}", tags=['processes'], response_model=ProcessModel) -def process_query(process_id: int = Path(description="Internal, the unique numeric id of this process.", default=None, example=1), - current_user: Optional[int] = Depends(get_optional_current_user)) \ +def process_query( + process_id: int = Path(description="Internal, the unique numeric id of this process.", default=None, example=1), + current_user: Optional[int] = Depends(get_optional_current_user)) \ -> ProcessBO: """ Returns **information about the process** corresponding to the given id. @@ -1071,8 +1086,8 @@ def process_query(process_id: int = Path(description="Internal, the unique numer @app.post("/object_set/{project_id}/query", tags=['objects'], response_model=ObjectSetQueryRsp, response_class=MyORJSONResponse # Force the ORJSON encoder ) -def get_object_set(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - filters: ProjectFiltersModel = Body(title="Project filters Model", default=None), +def get_object_set(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + filters: ProjectFiltersModel = Body(...), fields: Optional[str] = Query(title="Fields", description=''' Specify the needed object (and ancilliary entities) fields. @@ -1083,10 +1098,16 @@ def get_object_set(project_id: int = Path(description="Internal, numeric id of Use a comma to separate fields. ''', default=None, example="obj.longitude, obj.latitude, obj.imgcount"), - order_field: Optional[str] = Query(title="Order field", description='order_field will order the result using given field. If prefixed with "-" then it will be reversed.', default=None, example="obj.imgcount"), + order_field: Optional[str] = Query(title="Order field", + description='order_field will order the result using given field. If prefixed with "-" then it will be reversed.', + default=None, example="obj.imgcount"), # TODO: order_field should be a user-visible field name, not nXXX, in case of free field - window_start: Optional[int] = Query(default=None, title="Window start", description="Allows to return only a slice of the result. Skip window_start before returning data.", example="10"), - window_size: Optional[int] = Query(default=None, title="Window size", description="Allows to return only a slice of the result. Return only window_size lines.", example="100"), + window_start: Optional[int] = Query(default=None, title="Window start", + description="Allows to return only a slice of the result. Skip window_start before returning data.", + example="10"), + window_size: Optional[int] = Query(default=None, title="Window size", + description="Allows to return only a slice of the result. Return only window_size lines.", + example="100"), current_user: Optional[int] = Depends(get_optional_current_user)) -> ObjectSetQueryRsp: """ Returns **filtred object Ids** for the given project. @@ -1112,10 +1133,11 @@ def get_object_set(project_id: int = Path(description="Internal, numeric id of @app.post("/object_set/{project_id}/summary", tags=['objects'], response_model=ObjectSetSummaryRsp) -def get_object_set_summary(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - only_total: bool = Query(title="Only total", description="#TODO JCE", default=None), - filters: ProjectFiltersModel = Body(title="Project filters Model", default=None), - current_user: Optional[int] = Depends(get_optional_current_user)) -> ObjectSetSummaryRsp: +def get_object_set_summary( + project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + only_total: bool = Query(title="Only total", description="#TODO JCE", default=None), + filters: ProjectFiltersModel = Body(...), + current_user: Optional[int] = Depends(get_optional_current_user)) -> ObjectSetSummaryRsp: """ For the given project, with given filters, return the classification summary, i.e.: - Total number of objects @@ -1133,9 +1155,10 @@ def get_object_set_summary(project_id: int = Path(description="Internal, numeric @app.post("/object_set/{project_id}/reset_to_predicted", tags=['objects'], response_model=None) -def reset_object_set_to_predicted(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - filters: ProjectFiltersModel = Body(title="Project filters Model", default=None), - current_user: int = Depends(get_current_user)) -> None: +def reset_object_set_to_predicted( + project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + filters: ProjectFiltersModel = Body(...), + current_user: int = Depends(get_current_user)) -> None: """ Reset to Predicted all objects for the given project with the filters. """ @@ -1146,11 +1169,12 @@ def reset_object_set_to_predicted(project_id: int = Path(description="Internal, @app.post("/object_set/{project_id}/revert_to_history", tags=['objects'], response_model=ObjectSetRevertToHistoryRsp) -def revert_object_set_to_history(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - filters: ProjectFiltersModel= Body(title="Project filters Model", default=None), - dry_run: bool=Query(default=None), - target: Optional[int] = None, - current_user: int = Depends(get_current_user)) -> ObjectSetRevertToHistoryRsp: +def revert_object_set_to_history( + project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + filters: ProjectFiltersModel = Body(...), + dry_run: bool = Query(default=None), + target: Optional[int] = None, + current_user: int = Depends(get_current_user)) -> ObjectSetRevertToHistoryRsp: """ Revert all objects for the given project, with the filters, to the target. - param `filters`: The set of filters to apply to get the target objects. @@ -1166,11 +1190,12 @@ def revert_object_set_to_history(project_id: int = Path(description="Internal, n @app.post("/object_set/{project_id}/reclassify", tags=['objects']) -def reclassify_object_set(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - filters: ProjectFiltersModel = Body(title="Project filters Model", default=None), - forced_id: ClassifIDT = Query(title="", default=None), - reason: str = Query(title="", default=None), - current_user: int = Depends(get_current_user)) -> int: +def reclassify_object_set( + project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + filters: ProjectFiltersModel = Body(...), + forced_id: ClassifIDT = Query(title="", default=None), + reason: str = Query(title="", default=None), + current_user: int = Depends(get_current_user)) -> int: """ Regardless of present classification or state, set the new classification for this object set. If the filter designates "all with given classification", add a TaxonomyChangeLog entry. @@ -1183,7 +1208,7 @@ def reclassify_object_set(project_id: int = Path(description="Internal, numeric @app.post("/object_set/update", tags=['objects']) -def update_object_set(req: BulkUpdateReq = Body(title="Update request Model", default=None), +def update_object_set(req: BulkUpdateReq = Body(...), current_user: int = Depends(get_current_user)) -> int: """ Update all the objects with given IDs and values @@ -1197,12 +1222,12 @@ def update_object_set(req: BulkUpdateReq = Body(title="Update request Model", de @app.post("/object_set/classify", tags=['objects']) -def classify_object_set(req: ClassifyReq = Body(title="Classify request Model", default=None), +def classify_object_set(req: ClassifyReq = Body(...), current_user: int = Depends(get_current_user)) -> int: """ Change classification and/or qualification for a set of objects. Current user needs at least Annotate right on all projects of specified objects. - """##**Returns the number of updated entities.**NULL upon success. + """ ##**Returns the number of updated entities.**NULL upon success. # TODO: Cannot classify anymore to deprecated taxon/category assert len(req.target_ids) == len(req.classifications), "Need the same number of objects and classifications" with ObjectManager() as sce: @@ -1216,7 +1241,7 @@ def classify_object_set(req: ClassifyReq = Body(title="Classify request Model", @app.post("/object_set/classify_auto", tags=['objects']) -def classify_auto_object_set(req: ClassifyAutoReq = Body(title="Classify auto request Model", default=None), +def classify_auto_object_set(req: ClassifyAutoReq = Body(...), current_user: int = Depends(get_current_user)) -> int: """ Set automatic classification of a set of objects. @@ -1254,8 +1279,8 @@ def query_object_set_parents(object_ids: ObjectIDListT, @app.post("/object_set/export", tags=['objects'], response_model=ExportRsp) -def export_object_set(filters: ProjectFiltersModel = Body(title="Project filters Model", default=None), - request: ExportReq = Body(title="Export request Model", default=None), +def export_object_set(filters: ProjectFiltersModel = Body(...), + request: ExportReq = Body(...), current_user: Optional[int] = Depends(get_optional_current_user)) -> ExportRsp: """ Start an export job for the given object set and options. @@ -1266,12 +1291,8 @@ def export_object_set(filters: ProjectFiltersModel = Body(title="Project filters @app.post("/object_set/predict", tags=['objects'], response_model=PredictionRsp) -def predict_object_set(filters: ProjectFiltersModel = Body(title="Filters", - description="Description of how to reduce project data.", - default=None), - request: PredictionReq = Body(title="Prediction Request", - description="How to predict, in details.", - default=None), +def predict_object_set(filters: ProjectFiltersModel = Body(...), + request: PredictionReq = Body(...), current_user: Optional[int] = Depends(get_optional_current_user)) -> PredictionRsp: """ Start a prediction AKA automatic classification for the given object set and options. @@ -1305,8 +1326,9 @@ def erase_object_set(object_ids: ObjectIDListT, @app.get("/object/{object_id}", tags=['object'], response_model=ObjectModel) -def object_query(object_id: int = Path(description="Internal, the unique numeric id of this object.", default=None, example=1), - current_user: Optional[int] = Depends(get_optional_current_user)) \ +def object_query( + object_id: int = Path(description="Internal, the unique numeric id of this object.", default=None, example=1), + current_user: Optional[int] = Depends(get_optional_current_user)) \ -> ObjectBO: """ Returns **information about the object** corresponding to the given id. @@ -1322,37 +1344,38 @@ def object_query(object_id: int = Path(description="Internal, the unique numeric @app.get("/object/{object_id}/history", tags=['object'], - responses={ + responses={ 200: { "content": { "application/json": { "example": [{ - "objid":264409236, - "classif_id":82399, - "classif_date":"2021-09-21T14:59:01.007110", - "classif_who":"null", - "classif_type":"A", - "classif_qual":"P", - "classif_score":0.085, - "user_name":"null", - "taxon_name":"Penilia avirostris" - },{ - "objid":264409236, - "classif_id":25828, - "classif_date":"2021-09-29T08:25:37.968095", - "classif_who":1267, - "classif_type":"M", - "classif_qual":"V", - "classif_score":"null", - "user_name":"User name", - "taxon_name":"Copepoda" - }] + "objid": 264409236, + "classif_id": 82399, + "classif_date": "2021-09-21T14:59:01.007110", + "classif_who": "null", + "classif_type": "A", + "classif_qual": "P", + "classif_score": 0.085, + "user_name": "null", + "taxon_name": "Penilia avirostris" + }, { + "objid": 264409236, + "classif_id": 25828, + "classif_date": "2021-09-29T08:25:37.968095", + "classif_who": 1267, + "classif_type": "M", + "classif_qual": "V", + "classif_score": "null", + "user_name": "User name", + "taxon_name": "Copepoda" + }] } } } - },response_model=List[HistoricalClassificationModel]) # type:ignore -def object_query_history(object_id: int = Path(description="Internal, the unique numeric id of this object.", default=None, example=1), - current_user: Optional[int] = Depends(get_optional_current_user)) \ + }, response_model=List[HistoricalClassificationModel]) # type:ignore +def object_query_history( + object_id: int = Path(description="Internal, the unique numeric id of this object.", default=None, example=1), + current_user: Optional[int] = Depends(get_optional_current_user)) \ -> List[HistoricalClassification]: """ Returns **information about the object's history** corresponding to the given id. @@ -1406,8 +1429,9 @@ async def reclassif_stats(taxa_ids: str, @app.get("/taxa/reclassification_history/{project_id}", tags=['Taxonomy Tree']) -async def reclassif_project_stats(project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), - current_user: Optional[int] = Depends(get_optional_current_user)) \ +async def reclassif_project_stats( + project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + current_user: Optional[int] = Depends(get_optional_current_user)) \ -> List[TaxonBO]: """ Dig into reclassification logs and return the associations source->target for previous reclassifications. @@ -1419,8 +1443,9 @@ async def reclassif_project_stats(project_id: int = Path(description="Internal, @app.get("/taxon/{taxon_id}", tags=['Taxonomy Tree'], response_model=TaxonModel) -async def query_taxa(taxon_id: int = Path(description="Internal, the unique numeric id of this taxon.", default=None, example=1), - _current_user: Optional[int] = Depends(get_optional_current_user)) \ +async def query_taxa( + taxon_id: int = Path(description="Internal, the unique numeric id of this taxon.", default=None, example=1), + _current_user: Optional[int] = Depends(get_optional_current_user)) \ -> Optional[TaxonBO]: """ Information about a single taxon, including its lineage. @@ -1431,8 +1456,9 @@ async def query_taxa(taxon_id: int = Path(description="Internal, the unique nume @app.get("/taxon/{taxon_id}/usage", tags=['Taxonomy Tree'], response_model=List[TaxonUsageModel]) -async def query_taxa_usage(taxon_id: int = Path(description="Internal, the unique numeric id of this taxon.", default=None, example=1), - _current_user: Optional[int] = Depends(get_optional_current_user)) \ +async def query_taxa_usage( + taxon_id: int = Path(description="Internal, the unique numeric id of this taxon.", default=None, example=1), + _current_user: Optional[int] = Depends(get_optional_current_user)) \ -> List[TaxonUsageModel]: """ Where a given taxon is used. Only validated uses are returned. @@ -1444,7 +1470,8 @@ async def query_taxa_usage(taxon_id: int = Path(description="Internal, the uniqu @app.get("/taxon_set/search", tags=['Taxonomy Tree'], response_model=List[TaxaSearchRsp]) async def search_taxa(query: str, - project_id: Optional[int] = Query(description="Internal, numeric id of the project.", default=None), + project_id: Optional[int] = Query(description="Internal, numeric id of the project.", + default=None), current_user: Optional[int] = Depends(get_optional_current_user)): """ Search for taxa by name. @@ -1480,8 +1507,9 @@ async def query_taxa_set(ids: str, @app.get("/taxon/central/{taxon_id}", tags=['Taxonomy Tree']) -async def get_taxon_in_central(taxon_id: int = Path(description="Internal, the unique numeric id of this taxon.", default=None, example=1), - _current_user: int = Depends(get_current_user)): +async def get_taxon_in_central( + taxon_id: int = Path(description="Internal, the unique numeric id of this taxon.", default=None, example=1), + _current_user: int = Depends(get_current_user)): """ Get EcoTaxoServer full record for this taxon. """ @@ -1529,7 +1557,8 @@ async def pull_taxa_update_from_central(_current_user: int = Depends(get_current @app.get("/worms/{aphia_id}", tags=['Taxonomy Tree'], include_in_schema=False, response_model=TaxonModel) -async def query_taxa_in_worms(aphia_id: int,#= Path(description="Internal, the unique numeric id of this user.", default=None)#TODO JCE +async def query_taxa_in_worms(aphia_id: int, + # = Path(description="Internal, the unique numeric id of this user.", default=None)#TODO JCE _current_user: Optional[int] = Depends(get_optional_current_user)) \ -> Optional[TaxonBO]: """ @@ -1592,7 +1621,8 @@ async def matching_with_worms_nice(request: Request, @app.get("/admin/images/{project_id}/digest", tags=['WIP'], include_in_schema=False, response_model=str) def digest_project_images(max_digests: Optional[int], - project_id: int = Path(description="Internal, numeric id of the project.", default=None, example=1), + project_id: int = Path(description="Internal, numeric id of the project.", default=None, + example=1), current_user: int = Depends(get_current_user)) -> str: """ Compute digests for images referenced from a project. @@ -1621,9 +1651,10 @@ def digest_images(max_digests: Optional[int], @app.get("/admin/images/cleanup1", tags=['WIP'], include_in_schema=False, response_model=str) -def cleanup_images_1(project_id: int = Query(description="Internal, numeric id of the project.", default=None, example=1), - max_deletes: Optional[int] = None, - current_user: int = Depends(get_current_user)) -> str: +def cleanup_images_1( + project_id: int = Query(description="Internal, numeric id of the project.", default=None, example=1), + max_deletes: Optional[int] = None, + current_user: int = Depends(get_current_user)) -> str: """ Remove duplicated images inside same object. Probably due to import update bug. """ @@ -1671,7 +1702,9 @@ def machine_learning_train(project_id: int = Query(default=None, title="Input pr # ######################## END OF ADMIN @app.get("/jobs/", tags=['jobs'], response_model=List[JobModel]) -def list_jobs(for_admin: bool = Query(title="For admin", description="If FALSE return the jobs for current user, else return all of them.", default=None, example=False), +def list_jobs(for_admin: bool = Query(title="For admin", + description="If FALSE return the jobs for current user, else return all of them.", + default=None, example=False), current_user: int = Depends(get_current_user)) -> List[JobBO]: """ **Return the jobs** for current user, or all of them if admin is asked for. @@ -1695,18 +1728,19 @@ def get_job(job_id: int = Path(description="Internal, the unique numeric id of t @app.post("/jobs/{job_id}/answer", tags=['jobs'], - responses={ - 200: { - "content": { - "application/json": { - "example": null - } - } - } - }) -def reply_job_question(job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), - reply: Dict[str, Any]= Body(title="#TODO JCE Reply Model", default=None), - current_user: int = Depends(get_current_user)) -> None: + responses={ + 200: { + "content": { + "application/json": { + "example": null + } + } + } + }) +def reply_job_question( + job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), + reply: Dict[str, Any] = Body(title="#TODO JCE Reply Model", default=None), + current_user: int = Depends(get_current_user)) -> None: """ **Send answers to last question.** The job resumes after it receives the reply. @@ -1725,17 +1759,18 @@ def reply_job_question(job_id: int = Path(description="Internal, the unique nume @app.get("/jobs/{job_id}/restart", tags=['jobs'], - responses={ - 200: { - "content": { - "application/json": { - "example": null - } - } - } - }) -def restart_job(job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), - current_user: int = Depends(get_current_user)): + responses={ + 200: { + "content": { + "application/json": { + "example": null + } + } + } + }) +def restart_job( + job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), + current_user: int = Depends(get_current_user)): """ **Restart the job related to the given id.** @@ -1749,8 +1784,9 @@ def restart_job(job_id: int = Path(description="Internal, the unique numeric id @app.get("/jobs/{job_id}/log", tags=['jobs']) -def get_job_log_file(job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), - current_user: int = Depends(get_current_user)) -> FileResponse: +def get_job_log_file( + job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), + current_user: int = Depends(get_current_user)) -> FileResponse: """ **Return the log file produced by given job.** @@ -1769,8 +1805,9 @@ def get_job_log_file(job_id: int = Path(description="Internal, the unique numeri "description": "Return the produced file.", } }) -def get_job_file(job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), - current_user: int = Depends(get_current_user)) -> StreamingResponse: +def get_job_file( + job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), + current_user: int = Depends(get_current_user)) -> StreamingResponse: """ **Return the file produced by given job.** @@ -1784,8 +1821,9 @@ def get_job_file(job_id: int = Path(description="Internal, the unique numeric id @app.delete("/jobs/{job_id}", tags=['jobs']) -def erase_job(job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), - current_user: int = Depends(get_current_user)) -> int: +def erase_job( + job_id: int = Path(description="Internal, the unique numeric id of this job.", default=None, example=47445), + current_user: int = Depends(get_current_user)) -> int: """ **Delete the job** from DB, with associated storage.