Skip to content

Commit

Permalink
WIP improving API documentation #24
Browse files Browse the repository at this point in the history
  • Loading branch information
juliecoust committed Oct 6, 2021
1 parent 5cfb2f8 commit 5218cf8
Show file tree
Hide file tree
Showing 9 changed files with 1,691 additions and 693 deletions.
1,688 changes: 1,252 additions & 436 deletions openapi.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions py/API_models/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

class Constants(BaseModel):
""" Values which can be considered identical over the lifetime of the back-end """
license_texts: Dict[str, str] = Field(title="The supported licenses and help text/links",
default={short: expl for short, expl in DataLicense.EXPLANATIONS.items()})
app_manager: List[str] = Field(title="The application manager identity (name, mail), from config file",
default=["", ""], min_items=2, max_items=2)
license_texts: Dict[str, str] = Field(title="License texts", description="The supported licenses and help text/links",
default={short: expl for short, expl in DataLicense.EXPLANATIONS.items()}, example={"CC0 1.0":"<a href=\"https://creativecommons.org/publicdomain/zero/1.0/\" rel=\"nofollow\"><strong>CC-0</strong></a>: all registered EcoTaxa users are free to download, redistribute, modify, and build upon the data, with no conditions. Other databases can index the data. The data falls into the worldwide public domain. This is the license preferred by <a href=\"https://obis.org/manual/policy/\" rel=\"nofollow\">OBIS</a> and <a href=\"https://www.gbif.org/terms\" rel=\"nofollow\">GBIF</a>.","CC BY 4.0":"<a href=\"https://creativecommons.org/licenses/by/4.0/\" rel=\"nofollow\"><strong>CC-BY</strong></a>: all registered EcoTaxa users are free to download, redistribute, modify, and build upon the data, as long as they cite the dataset and its authors. Other databases can index the data.","CC BY-NC 4.0":"<a href=\"https://creativecommons.org/licenses/by-nc/4.0/\" rel=\"nofollow\"><strong>CC-BY-NC</strong></a>: all registered EcoTaxa users are free to download, redistribute, modify, and build upon the data, as long as they cite the dataset and its authors, and do not use it for commercial purpose (\"primarily intended for or directed toward commercial advantage or monetary compensation\"). Other databases can index the data.","Copyright":"<strong>Copyright</strong>: only contributors to this project have rights on this data. This prevents its distribution in any kind of database.","":"Not chosen"})
app_manager: List[str] = Field(title="Application manager", description="The application manager identity (name, mail), from config file",
default=["", ""], min_items=2, max_items=2, example=["App manager Name","[email protected]"])
184 changes: 115 additions & 69 deletions py/API_models/crud.py

Large diffs are not rendered by default.

19 changes: 6 additions & 13 deletions py/API_models/helpers/DataclassToModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#
import dataclasses
import datetime
from typing import Optional, TypeVar, Dict, List
from typing import Optional, TypeVar, Dict, List, Any
# noinspection PyUnresolvedReferences,PyProtectedMember
from typing import _GenericAlias # type: ignore

Expand All @@ -26,8 +26,7 @@ class DataclassConfig(BaseConfig):
T = TypeVar('T')


def dataclass_to_model(clazz: T, add_suffix: bool = False, titles: Optional[Dict[str, str]] = None,
descriptions: Optional[Dict[str, str]] = None) -> PydanticModelT:
def dataclass_to_model(clazz: T, add_suffix: bool = False, field_infos: Optional[Dict[str, Any]] = None) -> PydanticModelT:
model_fields = {}
a_field: dataclasses.Field
for a_field in dataclasses.fields(clazz):
Expand Down Expand Up @@ -65,15 +64,9 @@ def dataclass_to_model(clazz: T, add_suffix: bool = False, titles: Optional[Dict
ret: PydanticModelT = create_model(
model_name, __config__=DataclassConfig, **model_fields # type: ignore
)
if titles is not None:
# Amend with title, for doc. Let crash (KeyError) if titles are not up-to-date with base.
for a_field_name, a_title in titles.items():
the_field: ModelField = ret.__fields__[a_field_name]
the_field.field_info.title = a_title
if descriptions is not None:
# Amend with descriptions, for doc. Let crash (KeyError) if descriptions are not up-to-date with base.
for a_field_name, a_description in descriptions.items():
if field_infos is not None:
# Amend with Field() calls, for doc. Let crash (KeyError) if desync with base.
for a_field_name, a_field_info in field_infos.items():
the_desc_field: ModelField = ret.__fields__[a_field_name]
the_desc_field.field_info.description = a_description

the_desc_field.field_info = a_field_info
return ret
12 changes: 10 additions & 2 deletions py/API_models/helpers/TypedDictToModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@
#
# https://github.com/samuelcolvin/pydantic/issues/760
#
from typing import Optional, TypeVar
from typing import Optional, TypeVar, Dict, Any

# noinspection PyPackageRequirements
from pydantic import create_model

from API_models.helpers import PydanticModelT
# noinspection PyPackageRequirements
from pydantic.fields import ModelField

# Generify the def with input type
T = TypeVar('T')


def typed_dict_to_model(typed_dict: T): # TODO -> Type[BaseModel]:
def typed_dict_to_model(typed_dict: T, field_infos: Optional[Dict[str, Any]] = None): # TODO -> Type[BaseModel]:
annotations = {}
for name, field in typed_dict.__annotations__.items():
if field == Optional[str]:
Expand All @@ -35,4 +37,10 @@ def typed_dict_to_model(typed_dict: T): # TODO -> Type[BaseModel]:
# Make the model get-able
# noinspection PyTypeHints
ret.get = lambda self, k, d: getattr(self, k, d) # type: ignore

if field_infos is not None:
# Amend with Field() calls, for doc. Let crash (KeyError) if desync with base.
for a_field_name, a_field_info in field_infos.items():
the_desc_field: ModelField = ret.__fields__[a_field_name]
the_desc_field.field_info = a_field_info
return ret
4 changes: 2 additions & 2 deletions py/API_models/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@


class LoginReq(BaseModel):
password: str = Field(title="User's password" , default=None, description="User password", example="UserPassword!")
username: str = Field(title="User's eamil", default=None, description="User email used during registration", example="user@email.com")
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")

112 changes: 81 additions & 31 deletions py/API_models/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,83 @@
from DB import Image, ObjectHeader
from .helpers.pydantic import ResponseModel

ObjectHeaderModel = sqlalchemy_to_pydantic(ObjectHeader)

#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=""),
"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"),
"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//")

}
ObjectHeaderModel = sqlalchemy_to_pydantic(ObjectHeader, field_infos=_DBObjectHeaderDescription)


class ObjectFieldsModel(BaseModel):
pass

#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")
}

_ImageModelFromDB = sqlalchemy_to_pydantic(Image)
_ImageModelFromDB = sqlalchemy_to_pydantic(Image, field_infos=_DBImageDescription)


class ImageModel(_ImageModelFromDB): # type:ignore
pass


class ObjectModel(ObjectHeaderModel, ObjectFieldsModel): # type:ignore
orig_id: str = Field(title="Original object ID from initial TSV load")
object_link: Optional[str] = Field(title="Object link")
sample_id: int = Field(title="Sample (i.e. parent of parent acquisition) ID")
project_id: int = Field(title="Project (i.e. parent of sample) ID")
images: List[ImageModel] = Field(title="Images for this object",
default=[])
free_columns: Dict[str, Any] = Field(title="Free columns from object mapping in project",
default={})
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={})


class ObjectSetQueryRsp(ResponseModel):
object_ids: ObjectIDListT = Field(title="Matching object IDs", default=[])
acquisition_ids: List[Optional[int]] = Field(title="Parent (acquisition) IDs", default=[])
sample_ids: List[Optional[int]] = Field(title="Parent (sample) IDs", default=[])
project_ids: List[Optional[int]] = Field(title="Project IDs", default=[])
details: List[List] = Field(title="Requested fields, in request order", default=[])
total_ids: int = Field(title="Total rows returned by the query, even if it was window-ed", default=0)
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 number of objects in the set", default=None)
validated_objects: Optional[int] = Field(title="Number of validated objects in the set", default=None)
dubious_objects: Optional[int] = Field(title="Number of dubious objects in the set", default=None)
predicted_objects: Optional[int] = Field(title="Number of predicted objects in the set", default=None)
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)
Expand All @@ -62,25 +100,25 @@ 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="Object + last classification", # type: ignore
last_entries: List[HistoricalLastClassificationModel] = Field(title="Last entries", description="Object + last classification", # type: ignore
default=[])
# TODO: Below is ClassifSetInfoT but this defeats openapi generator
classif_info: Dict[int, Any] = Field(title="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
class ClassifyReq(BaseModel):
target_ids: List[int] = Field(title="The IDs of the target objects")
classifications: List[int] = Field(title="The wanted new classifications, i.e. taxon ID, one for each object. "
" Use -1 to keep present one.")
wanted_qualification: str = Field(title="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 ClassifyAutoReq(BaseModel):
target_ids: List[int] = Field(title="The IDs of the target objects")
classifications: List[int] = Field(title="The wanted new classifications, i.e. taxon ID, one for each object. ")
scores: List[float] = Field(title="The classification scores, generally b/w 0 and 1. ")
keep_log: bool = Field(title="Set if former automatic classification history is needed. ")
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.")
keep_log: bool = Field(title="Keep log", description="Set if former automatic classification history is needed.")
class Config:
schema_extra = {
"example": {
Expand All @@ -91,7 +129,19 @@ class Config:
}
}

HistoricalClassificationModel = dataclass_to_model(HistoricalClassification)
#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"),
"taxon_name": Field(title="Taxon name", description="The taxon name of the object.", example="Penilia avirostris")
}
HistoricalClassificationModel = dataclass_to_model(HistoricalClassification, field_infos=_DBHistoricalClassificationDescription)


class ObjectHistoryRsp(ResponseModel):
Expand Down
Loading

0 comments on commit 5218cf8

Please sign in to comment.