init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
"""BrAPI 2.x 只读互操作接口 + MIAPPE v1.1 元数据导出(批3 底座#8)。
|
||||
|
||||
不建表、不写库:仅将 bre_germplasm / bre_trait / bre_trial_study /
|
||||
bre_trait_observation / bre_observation 映射为 BrAPI 2.x 资源,
|
||||
/miappe 导出 MIAPPE v1.1 元数据模板。
|
||||
响应按 BrAPI 规范以 metadata+pagination+result 信封直接返回,
|
||||
不套用系统 ResponseSchema(避免破坏外部 BrAPI 客户端解析)。
|
||||
"""
|
||||
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_schema import AuthSchema
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
|
||||
from ..environment_condition.model import EnvironmentConditionModel
|
||||
from ..germplasm.model import BreedingGermplasmModel
|
||||
from ..observation.model import ObservationModel
|
||||
from ..site.model import BreedingSiteModel
|
||||
from ..trait.model import TraitModel
|
||||
from ..trait_observation.model import TraitObservationModel
|
||||
from ..tree.model import TreeModel
|
||||
from ..trial_study.model import TrialStudyModel
|
||||
|
||||
brapi_router = APIRouter(prefix="/brapi/v2", tags=["BrAPI 互操作"])
|
||||
|
||||
COMMON_CROP = "Peach"
|
||||
_GENUS = "Prunus"
|
||||
_SPECIES = "persica"
|
||||
_MAX_PAGE_SIZE = 10000
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 通用工具
|
||||
def _num(v):
|
||||
"""Numeric/Decimal → float(JSON 可序列化);None 透传。"""
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, Decimal):
|
||||
return float(v)
|
||||
if isinstance(v, float) and v.is_integer():
|
||||
return int(v)
|
||||
return v
|
||||
|
||||
|
||||
def _clean(d: dict) -> dict:
|
||||
"""剔除 None 值(BrAPI 空字段不输出)。"""
|
||||
return {k: v for k, v in d.items() if v is not None}
|
||||
|
||||
|
||||
def _pagination(total: int, page: int, page_size: int) -> dict:
|
||||
total_pages = (total + page_size - 1) // page_size if total else 0
|
||||
return {
|
||||
"pageSize": page_size,
|
||||
"currentPage": page,
|
||||
"totalCount": total,
|
||||
"totalPages": total_pages,
|
||||
}
|
||||
|
||||
|
||||
def _envelope(total: int, page: int, page_size: int, data: list, info: str = "Success") -> dict:
|
||||
return {
|
||||
"metadata": {
|
||||
"pagination": _pagination(total, page, page_size),
|
||||
"status": [{"messageType": "INFO", "message": info}],
|
||||
"datafiles": [],
|
||||
},
|
||||
"result": {"data": data},
|
||||
}
|
||||
|
||||
|
||||
def _parse_categories(scale_json: str | None) -> list[str]:
|
||||
"""解析 bre_trait.scale_json 为 BrAPI scale.validValues.categories。"""
|
||||
if not scale_json:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(scale_json)
|
||||
except Exception:
|
||||
return []
|
||||
if isinstance(data, list):
|
||||
out = []
|
||||
for item in data:
|
||||
if isinstance(item, dict):
|
||||
label = item.get("label") or item.get("name") or item.get("value")
|
||||
if label is not None:
|
||||
out.append(str(label))
|
||||
else:
|
||||
out.append(str(item))
|
||||
return out
|
||||
if isinstance(data, dict):
|
||||
for key in ("categories", "levels", "values"):
|
||||
if isinstance(data.get(key), list):
|
||||
return _parse_categories(json.dumps(data[key]))
|
||||
return [str(k) for k in data.keys()]
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 映射函数
|
||||
def _germplasm_to_brapi(g: BreedingGermplasmModel) -> dict:
|
||||
additional = _clean({
|
||||
"varietyType": g.variety_type,
|
||||
"origin": g.origin,
|
||||
"avgFruitWeightG": _num(g.avg_fruit_weight),
|
||||
"sscPct": _num(g.ssc),
|
||||
"firmness": g.firmness,
|
||||
"maturityPeriod": g.maturity_period,
|
||||
"floweringPeriod": g.flowering_period,
|
||||
"chillingRequirementH": g.chilling_requirement,
|
||||
"diseaseResistance": g.disease_resistance,
|
||||
"sAlleles": g.s_alleles,
|
||||
"canBeFemale": g.can_be_female,
|
||||
"canBeMale": g.can_be_male,
|
||||
"pedigreeNote": g.pedigree_note,
|
||||
"photoPath": g.photo_path,
|
||||
"stage": g.stage,
|
||||
"generation": g.generation,
|
||||
"storageType": g.storage_type,
|
||||
"isRootstock": g.is_rootstock,
|
||||
"rootstockId": g.rootstock_id,
|
||||
"instituteCode": g.institute_code,
|
||||
"collectionSite": g.collection_site,
|
||||
"biologicalStatus": g.biological_status,
|
||||
"breedingProgram": g.breeding_program,
|
||||
})
|
||||
return _clean({
|
||||
"germplasmDbId": str(g.id),
|
||||
"germplasmName": g.cultivar_name,
|
||||
"germplasmPUI": str(g.uuid) if g.uuid else None,
|
||||
"accessionNumber": g.accession_no,
|
||||
"commonCropName": COMMON_CROP,
|
||||
"genus": _GENUS,
|
||||
"species": _SPECIES,
|
||||
"subtaxa": g.variety_type,
|
||||
"taxonIds": [{"sourceName": "bre_germplasm", "taxonId": g.variety_type}] if g.variety_type else [],
|
||||
"acquisitionDate": g.acquisition_date.isoformat() if g.acquisition_date else None,
|
||||
"countryOfOriginCode": g.country_origin,
|
||||
"collection": g.preservation_site,
|
||||
"pedigree": g.pedigree_note,
|
||||
"synonyms": [],
|
||||
"additionalInfo": additional,
|
||||
"externalReferences": (
|
||||
[{"referenceID": str(g.uuid), "referenceSource": "bre_germplasm"}] if g.uuid else []
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
def _trait_to_brapi(t: TraitModel) -> dict:
|
||||
dt_map = {"numeric": "Numerical", "date": "Date", "text": "Text"}
|
||||
dt = dt_map.get(t.data_type, "Text")
|
||||
categories = _parse_categories(t.scale_json)
|
||||
valid_values: dict = {}
|
||||
if t.valid_min is not None:
|
||||
valid_values["min"] = _num(t.valid_min)
|
||||
if t.valid_max is not None:
|
||||
valid_values["max"] = _num(t.valid_max)
|
||||
if categories:
|
||||
valid_values["categories"] = categories
|
||||
if not valid_values and t.unit:
|
||||
valid_values["value"] = t.unit
|
||||
return _clean({
|
||||
"observationVariableDbId": str(t.id),
|
||||
"observationVariableName": t.trait_name,
|
||||
"observationVariablePUI": t.ontology_uri,
|
||||
"commonCropName": COMMON_CROP,
|
||||
"trait": _clean({
|
||||
"traitDbId": str(t.id),
|
||||
"traitName": t.trait_name,
|
||||
"class": t.category,
|
||||
"description": t.remark,
|
||||
"synonyms": [],
|
||||
}),
|
||||
"method": _clean({
|
||||
"methodDbId": f"m{t.id}",
|
||||
"methodName": t.method or t.trait_name,
|
||||
"methodPUI": t.method_uri,
|
||||
"description": t.method,
|
||||
}),
|
||||
"scale": _clean({
|
||||
"scaleDbId": f"s{t.id}",
|
||||
"scaleName": t.trait_name,
|
||||
"scalePUI": t.ontology_uri,
|
||||
"dataType": dt,
|
||||
"validValues": valid_values,
|
||||
}),
|
||||
"growthStage": _clean({
|
||||
"growthStageDbId": t.stage,
|
||||
"growthStageName": t.stage,
|
||||
}),
|
||||
"defaultValue": None,
|
||||
"synonyms": [],
|
||||
})
|
||||
|
||||
|
||||
def _study_to_brapi(s: TrialStudyModel, site_name: str | None) -> dict:
|
||||
seasons = [{"seasonDbId": str(s.year), "season": str(s.year)}] if s.year else []
|
||||
return _clean({
|
||||
"studyDbId": str(s.id),
|
||||
"studyName": s.study_name,
|
||||
"studyDescription": s.remark,
|
||||
"studyType": "Field Trial",
|
||||
"studyDesign": s.design_type,
|
||||
"startDate": f"{s.year}-01-01" if s.year else None,
|
||||
"active": True,
|
||||
"trialDbId": str(s.trial_id),
|
||||
"locationDbId": str(s.site_id) if s.site_id else None,
|
||||
"locationName": site_name,
|
||||
"seasons": seasons,
|
||||
"commonCropName": COMMON_CROP,
|
||||
"additionalInfo": _clean({
|
||||
"blockCount": s.block_count,
|
||||
"season": s.season,
|
||||
"designType": s.design_type,
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
def _observation_to_brapi(
|
||||
obs_id: int,
|
||||
trait_id: int | None,
|
||||
trait_name: str | None,
|
||||
tree_id: int | None,
|
||||
tree_no: str | None,
|
||||
germplasm_id: int | None,
|
||||
germplasm_name: str | None,
|
||||
study_id: int | None,
|
||||
study_name: str | None,
|
||||
value: str | None,
|
||||
time_stamp: str | None,
|
||||
season_year: int | None,
|
||||
additional: dict | None = None,
|
||||
) -> dict:
|
||||
return _clean({
|
||||
"observationDbId": str(obs_id),
|
||||
"observationVariableDbId": str(trait_id) if trait_id else None,
|
||||
"observationVariableName": trait_name,
|
||||
"studyDbId": str(study_id) if study_id else None,
|
||||
"studyName": study_name,
|
||||
"germplasmDbId": str(germplasm_id) if germplasm_id else None,
|
||||
"germplasmName": germplasm_name or tree_no,
|
||||
"observationUnitDbId": f"tree:{tree_id}" if tree_id else None,
|
||||
"observationUnitName": tree_no,
|
||||
"observationTimeStamp": time_stamp,
|
||||
"value": value if value is not None else "",
|
||||
"season": {"seasonDbId": str(season_year), "season": str(season_year)} if season_year else None,
|
||||
"additionalInfo": additional or {},
|
||||
})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 端点
|
||||
@brapi_router.get("/germplasm", summary="BrAPI 种质资源(bre_germplasm 映射)")
|
||||
async def brapi_germplasm(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission([]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: int = Query(0, ge=0, description="页码(0基)"),
|
||||
pageSize: int = Query(1000, ge=1, le=_MAX_PAGE_SIZE, description="页大小"),
|
||||
germplasmDbId: str | None = Query(None, description="按种质ID过滤"),
|
||||
germplasmName: str | None = Query(None, description="按名称模糊过滤"),
|
||||
) -> JSONResponse:
|
||||
filters = [BreedingGermplasmModel.is_deleted == False]
|
||||
if germplasmDbId and germplasmDbId.isdigit():
|
||||
filters.append(BreedingGermplasmModel.id == int(germplasmDbId))
|
||||
if germplasmName:
|
||||
filters.append(BreedingGermplasmModel.cultivar_name.ilike(f"%{germplasmName}%"))
|
||||
total = (await db.execute(select(func.count()).select_from(BreedingGermplasmModel).where(*filters))).scalar() or 0
|
||||
rows = (
|
||||
await db.execute(
|
||||
select(BreedingGermplasmModel).where(*filters)
|
||||
.order_by(BreedingGermplasmModel.id)
|
||||
.offset(page * pageSize).limit(pageSize)
|
||||
)
|
||||
).scalars().all()
|
||||
data = [_germplasm_to_brapi(g) for g in rows]
|
||||
return JSONResponse(content=_envelope(total, page, pageSize, data, f"{total} 条种质"))
|
||||
|
||||
|
||||
@brapi_router.get("/observationvariables", summary="BrAPI 观测变量(bre_trait 映射)")
|
||||
async def brapi_observationvariables(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission([]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: int = Query(0, ge=0, description="页码(0基)"),
|
||||
pageSize: int = Query(1000, ge=1, le=_MAX_PAGE_SIZE, description="页大小"),
|
||||
observationVariableDbId: str | None = Query(None, description="按观测变量ID过滤"),
|
||||
observationVariableName: str | None = Query(None, description="按变量名称模糊过滤"),
|
||||
) -> JSONResponse:
|
||||
filters = [TraitModel.is_deleted == False]
|
||||
if observationVariableDbId and observationVariableDbId.isdigit():
|
||||
filters.append(TraitModel.id == int(observationVariableDbId))
|
||||
if observationVariableName:
|
||||
filters.append(TraitModel.trait_name.ilike(f"%{observationVariableName}%"))
|
||||
total = (await db.execute(select(func.count()).select_from(TraitModel).where(*filters))).scalar() or 0
|
||||
rows = (
|
||||
await db.execute(
|
||||
select(TraitModel).where(*filters)
|
||||
.order_by(TraitModel.id)
|
||||
.offset(page * pageSize).limit(pageSize)
|
||||
)
|
||||
).scalars().all()
|
||||
data = [_trait_to_brapi(t) for t in rows]
|
||||
return JSONResponse(content=_envelope(total, page, pageSize, data, f"{total} 条观测变量"))
|
||||
|
||||
|
||||
@brapi_router.get("/studies", summary="BrAPI 试验研究(bre_trial_study 映射)")
|
||||
async def brapi_studies(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission([]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: int = Query(0, ge=0, description="页码(0基)"),
|
||||
pageSize: int = Query(1000, ge=1, le=_MAX_PAGE_SIZE, description="页大小"),
|
||||
studyDbId: str | None = Query(None, description="按研究ID过滤"),
|
||||
studyName: str | None = Query(None, description="按研究名称模糊过滤"),
|
||||
trialDbId: str | None = Query(None, description="按试验ID过滤"),
|
||||
) -> JSONResponse:
|
||||
filters = [TrialStudyModel.is_deleted == False]
|
||||
if studyDbId and studyDbId.isdigit():
|
||||
filters.append(TrialStudyModel.id == int(studyDbId))
|
||||
if studyName:
|
||||
filters.append(TrialStudyModel.study_name.ilike(f"%{studyName}%"))
|
||||
if trialDbId and trialDbId.isdigit():
|
||||
filters.append(TrialStudyModel.trial_id == int(trialDbId))
|
||||
total = (await db.execute(select(func.count()).select_from(TrialStudyModel).where(*filters))).scalar() or 0
|
||||
rows = (
|
||||
await db.execute(
|
||||
select(TrialStudyModel).where(*filters)
|
||||
.order_by(TrialStudyModel.id)
|
||||
.offset(page * pageSize).limit(pageSize)
|
||||
)
|
||||
).scalars().all()
|
||||
site_ids = {s.site_id for s in rows if s.site_id}
|
||||
site_map: dict[int, str] = {}
|
||||
if site_ids:
|
||||
sites = (await db.execute(select(BreedingSiteModel).where(BreedingSiteModel.id.in_(site_ids)))).scalars().all()
|
||||
site_map = {s.id: s.site_name for s in sites}
|
||||
data = [_study_to_brapi(s, site_map.get(s.site_id)) for s in rows]
|
||||
return JSONResponse(content=_envelope(total, page, pageSize, data, f"{total} 条研究"))
|
||||
|
||||
|
||||
@brapi_router.get("/observations", summary="BrAPI 观测值(bre_trait_observation / bre_observation 合并)")
|
||||
async def brapi_observations(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission([]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: int = Query(0, ge=0, description="页码(0基)"),
|
||||
pageSize: int = Query(1000, ge=1, le=_MAX_PAGE_SIZE, description="页大小"),
|
||||
studyDbId: str | None = Query(None, description="按研究ID过滤"),
|
||||
germplasmDbId: str | None = Query(None, description="按种质ID过滤(树关联种质)"),
|
||||
observationVariableDbId: str | None = Query(None, description="按观测变量ID过滤"),
|
||||
) -> JSONResponse:
|
||||
study_id = int(studyDbId) if studyDbId and studyDbId.isdigit() else None
|
||||
var_id = int(observationVariableDbId) if observationVariableDbId and observationVariableDbId.isdigit() else None
|
||||
|
||||
tree_filter: set[int] | None = None
|
||||
if germplasmDbId and germplasmDbId.isdigit():
|
||||
tree_ids = set(
|
||||
(await db.execute(
|
||||
select(TreeModel.id).where(TreeModel.is_deleted == False, TreeModel.germplasm_id == int(germplasmDbId))
|
||||
)).scalars()
|
||||
)
|
||||
if not tree_ids:
|
||||
return JSONResponse(content=_envelope(0, page, pageSize, []))
|
||||
tree_filter = tree_ids
|
||||
|
||||
filters1 = [TraitObservationModel.is_deleted == False]
|
||||
filters2 = [ObservationModel.is_deleted == False]
|
||||
if study_id is not None:
|
||||
filters1.append(TraitObservationModel.trial_study_id == study_id)
|
||||
filters2.append(ObservationModel.trial_study_id == study_id)
|
||||
if var_id is not None:
|
||||
filters1.append(TraitObservationModel.trait_id == var_id)
|
||||
filters2.append(ObservationModel.trait_id == var_id)
|
||||
if tree_filter is not None:
|
||||
filters1.append(TraitObservationModel.tree_id.in_(tree_filter))
|
||||
filters2.append(ObservationModel.tree_id.in_(tree_filter))
|
||||
|
||||
rows1 = (await db.execute(select(TraitObservationModel).where(*filters1))).scalars().all()
|
||||
rows2 = (await db.execute(select(ObservationModel).where(*filters2))).scalars().all()
|
||||
|
||||
tree_ids_needed = {r.tree_id for r in rows1 if r.tree_id} | {r.tree_id for r in rows2 if r.tree_id}
|
||||
var_ids_needed = {r.trait_id for r in rows1 if r.trait_id} | {r.trait_id for r in rows2 if r.trait_id}
|
||||
study_ids_needed = {r.trial_study_id for r in rows1 if r.trial_study_id} | {r.trial_study_id for r in rows2 if r.trial_study_id}
|
||||
|
||||
tree_map: dict[int, TreeModel] = {}
|
||||
if tree_ids_needed:
|
||||
tree_map = {t.id: t for t in (await db.execute(select(TreeModel).where(TreeModel.id.in_(tree_ids_needed)))).scalars().all()}
|
||||
var_map: dict[int, TraitModel] = {}
|
||||
if var_ids_needed:
|
||||
var_map = {t.id: t for t in (await db.execute(select(TraitModel).where(TraitModel.id.in_(var_ids_needed)))).scalars().all()}
|
||||
study_map: dict[int, TrialStudyModel] = {}
|
||||
if study_ids_needed:
|
||||
study_map = {s.id: s for s in (await db.execute(select(TrialStudyModel).where(TrialStudyModel.id.in_(study_ids_needed)))).scalars().all()}
|
||||
|
||||
germ_ids = {tree_map[t].germplasm_id for t in tree_map if tree_map[t].germplasm_id}
|
||||
germ_map: dict[int, BreedingGermplasmModel] = {}
|
||||
if germ_ids:
|
||||
germ_map = {g.id: g for g in (await db.execute(select(BreedingGermplasmModel).where(BreedingGermplasmModel.id.in_(germ_ids)))).scalars().all()}
|
||||
|
||||
obs: list[tuple[tuple[str, str], dict]] = []
|
||||
for r in rows1:
|
||||
tree = tree_map.get(r.tree_id)
|
||||
trait = var_map.get(r.trait_id)
|
||||
study = study_map.get(r.trial_study_id)
|
||||
germ = germ_map.get(tree.germplasm_id) if tree and tree.germplasm_id else None
|
||||
value = str(r.value_numeric) if r.value_numeric is not None else (r.value_text or r.value_date)
|
||||
ts = f"{r.evaluate_year}-01-01" if r.evaluate_year else r.created_time.isoformat() if r.created_time else None
|
||||
item = _observation_to_brapi(
|
||||
obs_id=r.id, trait_id=r.trait_id, trait_name=trait.trait_name if trait else None,
|
||||
tree_id=r.tree_id, tree_no=tree.tree_no if tree else None,
|
||||
germplasm_id=tree.germplasm_id if tree else None,
|
||||
germplasm_name=germ.cultivar_name if germ else None,
|
||||
study_id=r.trial_study_id, study_name=study.study_name if study else None,
|
||||
value=value, time_stamp=ts, season_year=r.evaluate_year,
|
||||
additional=_clean({"source": "bre_trait_observation", "evaluationId": r.evaluation_id, "cropLoad": _num(r.crop_load), "remark": r.remark}),
|
||||
)
|
||||
obs.append(((ts or "", str(r.id)), item))
|
||||
for r in rows2:
|
||||
tree = tree_map.get(r.tree_id)
|
||||
trait = var_map.get(r.trait_id)
|
||||
study = study_map.get(r.trial_study_id)
|
||||
germ = germ_map.get(tree.germplasm_id) if tree and tree.germplasm_id else None
|
||||
ts = r.obs_date.isoformat() if r.obs_date else (f"{r.obs_year}-01-01" if r.obs_year else r.created_time.isoformat() if r.created_time else None)
|
||||
unit_db_id = f"tree:{r.tree_id}" if r.tree_id else (f"plot:{r.plot_id}" if r.plot_id else None)
|
||||
item = _clean({
|
||||
"observationDbId": f"eav-{r.id}",
|
||||
"observationVariableDbId": str(r.trait_id) if r.trait_id else None,
|
||||
"observationVariableName": trait.trait_name if trait else None,
|
||||
"studyDbId": str(r.trial_study_id) if r.trial_study_id else None,
|
||||
"studyName": study.study_name if study else None,
|
||||
"germplasmDbId": str(tree.germplasm_id) if tree and tree.germplasm_id else None,
|
||||
"germplasmName": (germ.cultivar_name if germ else None) or (tree.tree_no if tree else None),
|
||||
"observationUnitDbId": unit_db_id,
|
||||
"observationUnitName": tree.tree_no if tree else None,
|
||||
"observationTimeStamp": ts,
|
||||
"value": r.obs_value or "",
|
||||
"season": {"seasonDbId": str(r.obs_year), "season": str(r.obs_year)} if r.obs_year else None,
|
||||
"additionalInfo": _clean({"source": "bre_observation", "obsType": r.obs_type, "operatorId": r.operator_id, "remark": r.remark}),
|
||||
})
|
||||
obs.append(((ts or "", f"eav-{r.id}"), item))
|
||||
|
||||
obs.sort(key=lambda x: x[0])
|
||||
total = len(obs)
|
||||
page_items = [item for _, item in obs[page * pageSize:(page + 1) * pageSize]]
|
||||
return JSONResponse(content=_envelope(total, page, pageSize, page_items, f"{total} 条观测"))
|
||||
|
||||
|
||||
async def _build_miappe(db: AsyncSession, study: TrialStudyModel) -> dict:
|
||||
"""按单个 study 组装 MIAPPE v1.1 元数据模板。"""
|
||||
site = None
|
||||
if study.site_id:
|
||||
site = (await db.execute(select(BreedingSiteModel).where(BreedingSiteModel.id == study.site_id))).scalars().first()
|
||||
env_rows: list[EnvironmentConditionModel] = []
|
||||
if study.site_id and study.year:
|
||||
env_rows = (await db.execute(
|
||||
select(EnvironmentConditionModel).where(
|
||||
EnvironmentConditionModel.site_id == study.site_id,
|
||||
EnvironmentConditionModel.year == study.year,
|
||||
)
|
||||
)).scalars().all()
|
||||
obs_cnt = (await db.execute(
|
||||
select(func.count()).select_from(TraitObservationModel).where(TraitObservationModel.trial_study_id == study.id)
|
||||
)).scalar() or 0
|
||||
tree_cnt = (await db.execute(
|
||||
select(func.count()).select_from(TreeModel).where(TreeModel.trial_study_id == study.id)
|
||||
)).scalar() or 0
|
||||
trait_cnt = (await db.execute(
|
||||
select(func.count()).select_from(TraitModel).where(TraitModel.is_deleted == False)
|
||||
)).scalar() or 0
|
||||
|
||||
meteo = [
|
||||
_clean({
|
||||
"chillingHours": _num(e.chilling_hours),
|
||||
"growingDegreeDays": _num(e.growing_degree_days),
|
||||
"rainfallMm": _num(e.rainfall_mm),
|
||||
"tempAvgC": _num(e.temp_avg),
|
||||
"soilMoisture": _num(e.soil_moisture),
|
||||
"source": e.source,
|
||||
})
|
||||
for e in env_rows
|
||||
]
|
||||
geo = _clean({
|
||||
"site": site.site_name if site else None,
|
||||
"latitude": _num(site.latitude) if site else None,
|
||||
"longitude": _num(site.longitude) if site else None,
|
||||
"elevation": site.elevation if site else None,
|
||||
})
|
||||
return _clean({
|
||||
"specification": "MIAPPE v1.1",
|
||||
"investigation": {
|
||||
"investigationUniqueID": f"bre-trial:{study.trial_id}",
|
||||
"investigationTitle": study.study_name,
|
||||
"project": None,
|
||||
},
|
||||
"study": _clean({
|
||||
"studyUniqueID": f"bre-study:{study.id}",
|
||||
"studyTitle": study.study_name,
|
||||
"studyDesign": study.design_type,
|
||||
"studyDescription": study.remark,
|
||||
"plantStructure": None,
|
||||
"coordinateType": "field-trial",
|
||||
"geoLocation": geo,
|
||||
"cultureSystem": study.season,
|
||||
"environmentDescription": None,
|
||||
"meteorologicalCharacteristics": meteo,
|
||||
"soilCharacteristics": None,
|
||||
}),
|
||||
"biologicalMaterial": _clean({
|
||||
"organism": f"{_GENUS} {_SPECIES}",
|
||||
"organ": "fruit",
|
||||
"genotype": None,
|
||||
"pedigree": None,
|
||||
}),
|
||||
"environment": meteo,
|
||||
"phenotype": {
|
||||
"observationUnit": "tree",
|
||||
"observedVariablesCount": trait_cnt,
|
||||
"observationUnitsCount": tree_cnt,
|
||||
"measurementsCount": obs_cnt,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@brapi_router.get("/miappe", summary="MIAPPE v1.1 元数据模板导出")
|
||||
async def brapi_miappe(
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission([]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
studyDbId: str | None = Query(None, description="按研究ID导出 MIAPPE 模板"),
|
||||
page: int = Query(0, ge=0, description="页码(0基)"),
|
||||
pageSize: int = Query(100, ge=1, le=_MAX_PAGE_SIZE, description="页大小"),
|
||||
) -> JSONResponse:
|
||||
if studyDbId and studyDbId.isdigit():
|
||||
study = (await db.execute(
|
||||
select(TrialStudyModel).where(TrialStudyModel.id == int(studyDbId), TrialStudyModel.is_deleted == False)
|
||||
)).scalars().first()
|
||||
if not study:
|
||||
return JSONResponse(content=_envelope(0, page, pageSize, [], "未找到该研究"))
|
||||
template = await _build_miappe(db, study)
|
||||
return JSONResponse(content=_envelope(1, 0, pageSize, [template], "MIAPPE v1.1 模板"))
|
||||
studies = (await db.execute(
|
||||
select(TrialStudyModel).where(TrialStudyModel.is_deleted == False).order_by(TrialStudyModel.id)
|
||||
)).scalars().all()
|
||||
data = [
|
||||
{
|
||||
"studyDbId": str(s.id),
|
||||
"studyName": s.study_name,
|
||||
"miappeUrl": f"/brapi/v2/miappe?studyDbId={s.id}",
|
||||
}
|
||||
for s in studies
|
||||
]
|
||||
return JSONResponse(content=_envelope(len(data), page, pageSize, data, "选择 study 导出 MIAPPE 模板"))
|
||||
Reference in New Issue
Block a user