From 23a6b83a02a06e62903cab23819c2f9396acd6f5 Mon Sep 17 00:00:00 2001 From: grigo Date: Wed, 24 Jun 2026 08:07:50 +0300 Subject: [PATCH] added covarage --- api/app/core/viewshed.py | 15 +++++- .../routers/__pycache__/jobs.cpython-313.pyc | Bin 844 -> 3106 bytes api/app/routers/jobs.py | 50 +++++++++++++++++- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/api/app/core/viewshed.py b/api/app/core/viewshed.py index 4145217..6bcbd21 100644 --- a/api/app/core/viewshed.py +++ b/api/app/core/viewshed.py @@ -16,8 +16,9 @@ from rasterio.warp import ( reproject, transform, ) +from pyproj import Transformer from shapely.geometry import mapping, shape -from shapely.ops import unary_union +from shapely.ops import transform as shapely_transform, unary_union from app.core.dem import _dem_files, _sample_dataset from app.core.geo import GeoPoint @@ -122,6 +123,16 @@ def _run_gdal_viewshed( subprocess.run(command, check=True, capture_output=True, text=True) +def _to_wgs84(geom, src_crs: CRS | None): + if src_crs is None: + return geom + crs_text = src_crs.to_string() + if crs_text in {"EPSG:4326", "OGC:CRS84", "WGS84"}: + return geom + transformer = Transformer.from_crs(src_crs, "EPSG:4326", always_xy=True) + return shapely_transform(transformer.transform, geom) + + def _viewshed_geojson(viewshed_path: Path, observer: GeoPoint) -> dict[str, Any]: features: list[dict[str, Any]] = [] with rasterio.open(viewshed_path) as dataset: @@ -130,7 +141,7 @@ def _viewshed_geojson(viewshed_path: Path, observer: GeoPoint) -> dict[str, Any] for geom, value in shapes(mask, mask=mask.astype(bool), transform=dataset.transform): if int(value) != 1: continue - polygon = shape(geom) + polygon = _to_wgs84(shape(geom), dataset.crs) if polygon.is_empty: continue features.append( diff --git a/api/app/routers/__pycache__/jobs.cpython-313.pyc b/api/app/routers/__pycache__/jobs.cpython-313.pyc index 733192155bbe799836d47f5376e1925045f1ec4b..b5b04b21ec6547b35e176ffb6d892a5029e161d3 100644 GIT binary patch literal 3106 zcmcH*O>Y}j@a>1aw%1=t`jI9jb-$oaAxT>YNC=hEq=DGrq`Prdh$XYJH_2A1*Lu5d ziz|wADpiph4u~8$R8Wcb1it|B3y>`zQaxG_0))7^q$f_yTl?8fNedUAv^(?W&CJfs zn~&WL1bhhA_xfw+Z+j5>g>Kk#HIdCf0r?cENaYd;Q^+T{IFI=V#f5|r7qJ+3VOQLZ z-40DmNO2GL#J$+->|F_8T*k74yA%F+00$ggN(AE}9Eyi=ID*pR0W{qFB<@f>=}y&~ z=3}Dj>p|y5+?DQ7qRg~5RONI)^^fpP{s8a?D1S@S&Zo!)hq;CpDjDO|&=#p(OI$Sk z2W3b`IU>$#_HtAp-tpOK#Vp!7CV@BS=4MY{&guo*F!RK1*_vInqCD|W87sP?TLm+3 z>BJvbXOe6On*2+;ow0P=Hu6gr?B%$*&^qRcM32v#3nbV?GFZ==I7i$-%oqT%ZWnPr z+qB*BzzuH!p3TpBdQbDMvlL=J=D8G??L}yw@;Pf>WSBzbpI9oV@~SYvtKtZ+gRJ6x zf&sRwi)mNgqn8lUo~GZ)u&wf{qhUYPG-R7O)e8Kn)MsY5(Bq3me$GVC?DQ!Rqfx!5x! zy^Ha;p&C$wZJ$ztvZsB2V2fc%f-O*^t^7W28gJQ1^IPABf1ZfnL0?BzybWhfcb$Y6SLM9i6ay{$|+ zJwEwPX*X47SPgwf-msSST;zS-rd4AVS8O5_u|Yb=mAUDu@rk*%84by0jfzGzY#WPO z){dl<>BpF-W|ULYlar^Dr9@0QQFgz zTZ9(T_IflD4Prlajdt9If1R|wWMV_Hwvj}Dol4LKit|kESiNh_kwtRe%rnU?e-Y3Bf3$0@kJ_>4r z@K|Gz-DPw?(7QfW3mhy@-ID^VSMLXV?|1iYbRVmBAG^2r=rda{bEfz%mjw2-9sQyd1bcRb!Ll%zw_qk_0hHEn!ImAK78x=hOfWs z>%TKx^BpTs{3r*RhQ=p{Dr2c?*V(p)M;_!4uL(CsHsrlkd2dbbD^K2+ci%Xu0n-};Bzg=$x}trlGIhi{HuAG>+-`pK=l17Jes z?7JIhGu5-1O6tAJs8;P-ctW(C*n}XWl6UY5L>h*~2QXKHkPglS%3OfLS=oMGoCC4{ zXI?<9yOKBz`Z4W^n6^Q@6JAjf%M@>AI_{%XzL9YPO%-VIkX^VR01>gSE+ z#8TML(qnYiR#k2kBCUZ#^AbCrr{8jxvdwrrfMDdd#t-9!kqZrVgk!@9hBJ2v&9_EU zs&(RozvLP?Z<+a$t7tF2^eW_pN6$kSg&z;VrL38^p&vv&P8q`gOL1}0xJ+Ct<|Q3t zY8?@&IHhPgi+JfxWNiJijj0pO8^bUu(3TquID9O;cm&QzDNh1kaCP#Y_2+Byey2pP zU0J=dA$3=!?wZuIzC=qn)O&mKtN0i3JNDOC?_T}x(w(c7!I|2j*=p!a#d8LVZC$L% z&sD_doC}Ta#xH|f+S}~q6=R{<2^Te(83hB=Kb&@=k&Y&PfR%>jc+&9IrxgkV5R*k? zsnJ10l;G5kH9)fx(`zCflxpetq5&PRL3h%bc5mpHjbB5-vau6YypLKyv)75kUF2BZ z$ZXFTC@-$)C-DTF(_c30V|djCj^lnn@&nZUJBn3N>?icnG|6z^&LL3hJjg#CxvwtoD4AxFeT)DqO72OI&EJtBwGy_ij?KDIk8>q1Sgb c*(?4?joV-6MXqoC;3fhJK)w4Nx zS?}>8gMYx2;!lYeuMHv|1ieLa7pqs%)ZtO}s_VViZ&yF^_F1dd1gclX*62HX-Cms^ z(ewrqNLUwQhAg(wifyz5!&zM?cF|q*c9+KjMF1(g4tI?R6?&<+bMa4q9up^Z6Zf8# z@U)qT)Qz0Py9^`e-<^NZ2%A3uEP#50tzBZ;kIUU1six&b$&hJrAggRt?$#&bTK$gQ ztbd7Yu3K^cQP%4Zvd8`Bz3mq*x(Oy_73`O@NfBDw*((dFc_FKOIHuWGRbI_hW@v0v z!q7osaytfG0^NB!7(C0OJJEmL(87jU+2pS%Aq0V5A8e)dHV=IVWnQ59bEE#?J(!1gAD*>QkKEvAAM(6m-OMc4h6aRMe KrzP0ZLgqL1G=H!F diff --git a/api/app/routers/jobs.py b/api/app/routers/jobs.py index a787629..f4d235c 100644 --- a/api/app/routers/jobs.py +++ b/api/app/routers/jobs.py @@ -1,11 +1,49 @@ -from fastapi import APIRouter, HTTPException, status +import json +from pathlib import Path +from fastapi import APIRouter, HTTPException, status +from fastapi.responses import FileResponse, JSONResponse + +from app.config import get_settings from app.models.jobs import JobResponse -from app.services.jobs import get_job +from app.services.jobs import get_job, get_job_record router = APIRouter() +def _artifact_path(job_id: str) -> Path: + record = get_job_record(job_id) + if record is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={"code": "JOB_NOT_FOUND", "detail": "Job not found"}, + ) + if record.get("status") != "done": + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={"code": "JOB_NOT_READY", "detail": "Job is not finished yet"}, + ) + uri = (record.get("result") or {}).get("uri") + if not uri: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={"code": "ARTIFACT_NOT_FOUND", "detail": "Job has no artifact URI"}, + ) + path = Path(uri).resolve() + root = get_settings().jobs_output_path.resolve() + if root not in path.parents and path != root: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={"code": "ARTIFACT_FORBIDDEN", "detail": "Artifact path is outside jobs output"}, + ) + if not path.is_file(): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={"code": "ARTIFACT_MISSING", "detail": f"Artifact file not found: {path.name}"}, + ) + return path + + @router.get("/{job_id}", response_model=JobResponse) def read(job_id: str) -> JobResponse: job = get_job(job_id) @@ -15,3 +53,11 @@ def read(job_id: str) -> JobResponse: detail={"code": "JOB_NOT_FOUND", "detail": "Job not found"}, ) return job + + +@router.get("/{job_id}/artifact") +def read_artifact(job_id: str): + path = _artifact_path(job_id) + if path.suffix.lower() == ".json": + return JSONResponse(content=json.loads(path.read_text(encoding="utf-8"))) + return FileResponse(path)