Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/backend/src/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def get_geoserver_service() -> GeoServerService:

def get_current_user(session: DatafeederSessionDep, token: TokenDep) -> User:
try:
payload = jwt.decode(token, get_settings().SECRET_KEY, algorithms=[security.ALGORITHM]) # type: ignore[arg-type]
payload = jwt.decode(token, get_settings().SECRET_KEY, algorithms=[security.ALGORITHM])
token_data = TokenPayload(**payload)
except (InvalidTokenError, ValidationError):
raise HTTPException(
Expand Down
22 changes: 11 additions & 11 deletions apps/backend/src/api/routes/ingestion/staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,15 +803,15 @@ def _iso(value: Any) -> Any:
for col in df.columns:
series = df[col]
if pd.api.types.is_datetime64_any_dtype(series):
df[col] = series.astype(str) # type: ignore[misc]
df[col] = series.astype(str)
elif series.dtype == "object":
# Sample the first non-null value to decide the column's real type;
# an all-null column has nothing to serialize, so we skip it.
non_null = series.dropna() # type: ignore[misc]
if not non_null.empty and isinstance(non_null.iloc[0], date): # type: ignore[misc]
non_null = series.dropna()
if not non_null.empty and isinstance(non_null.iloc[0], date):
# Convert per-value (not astype) so any stray nulls survive as
# None instead of becoming the string "NaT"/"None".
df[col] = series.apply(_iso) # type: ignore[misc]
df[col] = series.apply(_iso)


def _detect_original_projection(
Expand Down Expand Up @@ -1165,15 +1165,15 @@ def get_staging_preview(
is_geographic = True

geometry_cols: list[str] = []
for col in transformed_data.columns: # type: ignore[misc]
if not transformed_data[col].empty: # type: ignore[misc]
for col in transformed_data.columns:
if not transformed_data[col].empty:
sample_item = transformed_data[col].iloc[0]
sample: Any = sample_item # type: ignore[misc]
sample: Any = sample_item

if isinstance(sample, BaseGeometry):
geometry_cols.append(col) # type: ignore[misc]
elif hasattr(sample_item, "wkt"): # type: ignore[misc]
geometry_cols.append(col) # type: ignore[misc]
geometry_cols.append(col)
elif hasattr(sample_item, "wkt"):
geometry_cols.append(col)

logger.info(f"Found geometry columns: {geometry_cols}")

Expand Down Expand Up @@ -1211,7 +1211,7 @@ def get_staging_preview(
geojson_data = None

return StagingPreviewResponse(
data=data, # type: ignore[misc]
data=data,
geojson=geojson_data,
is_geographic=is_geographic,
)
Expand Down
8 changes: 4 additions & 4 deletions apps/backend/src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,14 @@ def settings_customise_sources(

### Validators and computed fields

@computed_field # type: ignore[prop-decorator]
@computed_field
@property
def all_cors_origins(self) -> list[str]:
return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [
self.FRONTEND_HOST
]

@computed_field # type: ignore[prop-decorator]
@computed_field
@property
def POSTGRES_DATAFEEDER_URI(self) -> PostgresDsn:
return PostgresDsn.build(
Expand All @@ -205,7 +205,7 @@ def POSTGRES_DATAFEEDER_URI(self) -> PostgresDsn:
path=self.POSTGRES_DATAFEEDER_DB,
)

@computed_field # type: ignore[prop-decorator]
@computed_field
@property
def POSTGRES_DATA_URI(self) -> PostgresDsn:
return PostgresDsn.build(
Expand All @@ -217,7 +217,7 @@ def POSTGRES_DATA_URI(self) -> PostgresDsn:
path=self.POSTGRES_DATA_DB,
)

@computed_field # type: ignore[prop-decorator]
@computed_field
@property
def GEONETWORK_XML_RECORD_URL(self) -> str:
return f"{self.METADATA_PUBLIC_URL}/srv/api/records/{{metadata_id}}/formatters/xml"
Expand Down
8 changes: 3 additions & 5 deletions apps/backend/src/core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
def create_access_token(subject: str | Any, expires_delta: timedelta) -> str:
expire = datetime.now(timezone.utc) + expires_delta
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, get_settings().SECRET_KEY, algorithm=ALGORITHM) # type: ignore[arg-type]
encoded_jwt = jwt.encode(to_encode, get_settings().SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


Expand Down Expand Up @@ -79,7 +79,7 @@ def build_access_expr(
return literal(EffectiveAccess.ADMIN.value).label("access_level") # type: ignore[return-value]

conditions: list[tuple[Any, str]] = [
(IntegrityLink.integrity_owner == username, EffectiveAccess.OWNER.value), # type: ignore[arg-type]
(IntegrityLink.integrity_owner == username, EffectiveAccess.OWNER.value),
]

if group_ids:
Expand Down Expand Up @@ -130,9 +130,7 @@ def compute_effective_access(
"""
access_expr = build_access_expr(geo_ctx.username, group_ids, geo_ctx.is_administrator())
result = session.exec(
select(access_expr)
.select_from(IntegrityLink) # type: ignore[arg-type]
.where(IntegrityLink.id == integrity_link.id)
select(access_expr).select_from(IntegrityLink).where(IntegrityLink.id == integrity_link.id)
).first()

if result is None:
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/services/console_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def get_all_roles(self) -> list[dict[str, Any]]:
response = httpx.get(url, timeout=5.0)
response.raise_for_status()
try:
return response.json() # type: ignore[no-any-return]
return response.json()
except ValueError as exc:
logger.error("Console returned invalid JSON from %s", url)
raise ValueError(f"Console returned invalid JSON from {url}") from exc
Expand Down Expand Up @@ -145,7 +145,7 @@ def get_all_organizations(self) -> list[dict[str, Any]]:
response = httpx.get(url, timeout=5.0)
response.raise_for_status()
try:
return response.json() # type: ignore[no-any-return]
return response.json()
except ValueError as exc:
logger.error("Console returned invalid JSON from %s", url)
raise ValueError(f"Console returned invalid JSON from {url}") from exc
8 changes: 4 additions & 4 deletions apps/backend/src/services/geoserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from data_manipulation.geoserver import (
update_layer_bbox as dm_update_layer_bbox,
)
from geoservercloud import GeoServerCloud # type: ignore[import-untyped]
from geoservercloud import GeoServerCloud
from geoservercloud.models.common import MetadataLink
from pydantic import BaseModel

Expand Down Expand Up @@ -521,7 +521,7 @@ def acl_layer_get(self, layer_name: str, access_type: AclAccessType) -> list[str
List of roles if the rule exists, None otherwise.
"""
rule_key = f"{layer_name}.{access_type}"
rest_client = self.geoserver.rest_service.rest_client # type: ignore[attr-defined]
rest_client = self.geoserver.rest_service.rest_client
response = rest_client.get("/rest/security/acl/layers", headers=_ACL_HEADERS)
if response.status_code >= 400:
raise GeoServerAclError(response.status_code, response.text)
Expand All @@ -539,7 +539,7 @@ def _acl_write(
Raises:
GeoServerAclError: on any non-2xx HTTP response (except 409 on POST).
"""
rest_client = self.geoserver.rest_service.rest_client # type: ignore[attr-defined]
rest_client = self.geoserver.rest_service.rest_client
if method == "POST":
response = rest_client.post(
"/rest/security/acl/layers", json=body, headers=_ACL_HEADERS, params=params
Expand Down Expand Up @@ -589,7 +589,7 @@ def acl_layer_delete(self, layer_name: str, access_type: AclAccessType) -> None:
access_type: The access type to delete (READ or WRITE).
"""
rule_key = f"{layer_name}.{access_type}"
rest_client = self.geoserver.rest_service.rest_client # type: ignore[attr-defined]
rest_client = self.geoserver.rest_service.rest_client
response = rest_client.delete(f"/rest/security/acl/layers/{rule_key}", headers=_ACL_HEADERS)
if response.status_code >= 400:
raise GeoServerAclError(response.status_code, response.text)
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/services/metadata_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any

from geonetwork import GnApi # type: ignore[import-untyped]
from geonetwork import GnApi
from lxml import etree

if TYPE_CHECKING:
Expand Down Expand Up @@ -222,7 +222,7 @@ def publish_metadata(

# Parse JSON response and extract UUID
response_data: Any = response.json()
metadata_uuid: str = response_data.get("uuid") or response_data.get("id") # type: ignore[assignment]
metadata_uuid: str = response_data.get("uuid") or response_data.get("id")
logger.info(f"Published metadata with UUID: {metadata_uuid}")
except Exception as e:
logger.error(f"Failed to publish metadata: {e}", exc_info=True)
Expand Down
8 changes: 4 additions & 4 deletions apps/backend/tests/core/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,25 +409,25 @@ class TestBuildAccessExprShape:

def test_empty_group_ids_emits_only_owner_branch(self) -> None:
expr = build_access_expr("user1", [], is_admin=False)
case_expr = expr.element # type: ignore[attr-defined]
case_expr = expr.element
assert len(case_expr.whens) == 1 # owner only

def test_single_group_emits_owner_write_read(self) -> None:
expr = build_access_expr("user1", ["g1"], is_admin=False)
case_expr = expr.element # type: ignore[attr-defined]
case_expr = expr.element
assert len(case_expr.whens) == 3 # owner + write + read

def test_multiple_groups_do_not_duplicate_write_read_branches(self) -> None:
"""Regression guard: a per-id loop would emit 1 + 2*N branches and let
READ from an early group shadow WRITE from a later one."""
expr = build_access_expr("user1", ["g1", "g2", "g3"], is_admin=False)
case_expr = expr.element # type: ignore[attr-defined]
case_expr = expr.element
assert len(case_expr.whens) == 3 # still owner + write + read

def test_write_branch_precedes_read_branch(self) -> None:
"""WRITE must be checked before READ so WRITE takes priority."""
expr = build_access_expr("user1", ["g1", "g2"], is_admin=False)
case_expr = expr.element # type: ignore[attr-defined]
case_expr = expr.element
values = [value.value for _, value in case_expr.whens]
assert values.index("WRITE") < values.index("READ")

Expand Down
34 changes: 17 additions & 17 deletions apps/backend/tests/services/test_geoserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from src.models.integrity_link_rule import RuleValue
from src.services.geoserver import ( # type: ignore[attr-defined]
from src.services.geoserver import (
_ACL_HEADERS, # type: ignore[attr-defined]
ACL_ROLE_EVERYONE,
AclAccessType,
Expand Down Expand Up @@ -552,7 +552,7 @@ def service(self) -> GeoServerService:
return svc

def test_post_succeeds_directly(self, service: GeoServerService) -> None:
service.acl_layer_post = MagicMock() # type: ignore[method-assign]
service.acl_layer_post = MagicMock()

service.acl_layer_set_rule("geor.my_layer", AclAccessType.READ, ["ROLE_IMPORT"])

Expand All @@ -561,9 +561,9 @@ def test_post_succeeds_directly(self, service: GeoServerService) -> None:
)

def test_replaces_without_merging_on_409(self, service: GeoServerService) -> None:
service.acl_layer_post = MagicMock(side_effect=GeoServerAclError(409, "Conflict")) # type: ignore[method-assign]
service.acl_layer_get = MagicMock(return_value=["ROLE_EXISTING"]) # type: ignore[method-assign]
service.acl_layer_put = MagicMock() # type: ignore[method-assign]
service.acl_layer_post = MagicMock(side_effect=GeoServerAclError(409, "Conflict"))
service.acl_layer_get = MagicMock(return_value=["ROLE_EXISTING"])
service.acl_layer_put = MagicMock()

service.acl_layer_set_rule("geor.my_layer", AclAccessType.READ, ["ROLE_IMPORT"])

Expand All @@ -573,7 +573,7 @@ def test_replaces_without_merging_on_409(self, service: GeoServerService) -> Non
)

def test_reraises_non_409_error(self, service: GeoServerService) -> None:
service.acl_layer_post = MagicMock(side_effect=GeoServerAclError(500, "Server error")) # type: ignore[method-assign]
service.acl_layer_post = MagicMock(side_effect=GeoServerAclError(500, "Server error"))

with pytest.raises(GeoServerAclError) as exc_info:
service.acl_layer_set_rule("geor.my_layer", AclAccessType.READ, ["ROLE_IMPORT"])
Expand All @@ -597,9 +597,9 @@ def service(self) -> GeoServerService:
return svc

def test_updates_with_remaining_roles(self, service: GeoServerService) -> None:
service.acl_layer_get = MagicMock(return_value=["ROLE_IMPORT", ACL_ROLE_EVERYONE]) # type: ignore[method-assign]
service.acl_layer_put = MagicMock() # type: ignore[method-assign]
service.acl_layer_delete = MagicMock() # type: ignore[method-assign]
service.acl_layer_get = MagicMock(return_value=["ROLE_IMPORT", ACL_ROLE_EVERYONE])
service.acl_layer_put = MagicMock()
service.acl_layer_delete = MagicMock()

service.acl_layer_remove_rule("geor.my_layer", AclAccessType.READ, [ACL_ROLE_EVERYONE])

Expand All @@ -609,19 +609,19 @@ def test_updates_with_remaining_roles(self, service: GeoServerService) -> None:
service.acl_layer_delete.assert_not_called()

def test_deletes_when_no_roles_remain(self, service: GeoServerService) -> None:
service.acl_layer_get = MagicMock(return_value=[ACL_ROLE_EVERYONE]) # type: ignore[method-assign]
service.acl_layer_put = MagicMock() # type: ignore[method-assign]
service.acl_layer_delete = MagicMock() # type: ignore[method-assign]
service.acl_layer_get = MagicMock(return_value=[ACL_ROLE_EVERYONE])
service.acl_layer_put = MagicMock()
service.acl_layer_delete = MagicMock()

service.acl_layer_remove_rule("geor.my_layer", AclAccessType.READ, [ACL_ROLE_EVERYONE])

service.acl_layer_delete.assert_called_once_with("geor.my_layer", AclAccessType.READ)
service.acl_layer_put.assert_not_called()

def test_get_returns_none_treated_as_empty(self, service: GeoServerService) -> None:
service.acl_layer_get = MagicMock(return_value=None) # type: ignore[method-assign]
service.acl_layer_put = MagicMock() # type: ignore[method-assign]
service.acl_layer_delete = MagicMock() # type: ignore[method-assign]
service.acl_layer_get = MagicMock(return_value=None)
service.acl_layer_put = MagicMock()
service.acl_layer_delete = MagicMock()

service.acl_layer_remove_rule("geor.my_layer", AclAccessType.READ, [ACL_ROLE_EVERYONE])

Expand Down Expand Up @@ -794,7 +794,7 @@ def test_puts_correct_payload(self, service: GeoServerService, rest_client: Magi
mock_endpoints.featuretype.return_value = (
"http://gs/workspaces/myws/datastores/myds/featuretypes/my_table.json"
)
service.geoserver.rest_service.rest_endpoints = mock_endpoints # type: ignore[misc]
service.geoserver.rest_service.rest_endpoints = mock_endpoints
rest_client.put.return_value.status_code = 200

service.update_layer_title("myws", "myds", "my_table", "My Layer Title")
Expand All @@ -809,7 +809,7 @@ def test_uses_featuretype_endpoint_url(
) -> None:
"""The URL is built using rest_endpoints.featuretype(ws, ds, name)."""
mock_endpoints = MagicMock()
service.geoserver.rest_service.rest_endpoints = mock_endpoints # type: ignore[misc]
service.geoserver.rest_service.rest_endpoints = mock_endpoints
rest_client.put.return_value.status_code = 200

service.update_layer_title("ws1", "ds1", "layer1", "Title")
Expand Down
2 changes: 2 additions & 0 deletions libs/data_manipulation/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dev = [
"poethepoet>0",
"pyright>0",
"pytest>0",
"pandas-stubs>=3.0.5.260730",
"types-geopandas>=1.1.4.20260807",
]

[tool.ruff]
Expand Down
Loading
Loading