diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd6dbeb..2121027 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,4 +16,5 @@ repos: rev: v0.910 hooks: - id: mypy - language_version: python \ No newline at end of file + language_version: python + args: [--install-types, --non-interactive] \ No newline at end of file diff --git a/oaff/app/oaff/app/data/sources/common/data_source_manager.py b/oaff/app/oaff/app/data/sources/common/data_source_manager.py index 2248726..17fa22e 100644 --- a/oaff/app/oaff/app/data/sources/common/data_source_manager.py +++ b/oaff/app/oaff/app/data/sources/common/data_source_manager.py @@ -6,5 +6,5 @@ class DataSourceManager(ABC): @abstractmethod - def get_data_sources() -> List[Type[DataSource]]: + def get_data_sources(self) -> List[Type[DataSource]]: pass diff --git a/oaff/app/oaff/app/data/sources/postgresql/stac_hybrid/postgresql_data_source.py b/oaff/app/oaff/app/data/sources/postgresql/stac_hybrid/postgresql_data_source.py index a5bd224..3753052 100644 --- a/oaff/app/oaff/app/data/sources/postgresql/stac_hybrid/postgresql_data_source.py +++ b/oaff/app/oaff/app/data/sources/postgresql/stac_hybrid/postgresql_data_source.py @@ -2,7 +2,7 @@ from hashlib import sha256 from logging import getLogger from os import path -from typing import Any, Coroutine, Dict, Final, List, Type +from typing import Any, Awaitable, Callable, Dict, Final, List, Type # geoalchemy import required for sa.MetaData reflection, even though unused in module import geoalchemy2 as ga # noqa: F401 @@ -51,7 +51,7 @@ class PostgresqlDataSource(DataSource): def __init__( self, connection_name: str, - connection_tester: Coroutine[None, None, Database], + connection_tester: Callable[[Database, str], Awaitable[None]], ): super().__init__(f"{self.DATA_SOURCE_NAME}:{connection_name}") self.db = Database(settings.url(connection_name)) @@ -155,7 +155,7 @@ def get_if_available( async def get_feature_set_provider( self, layer: PostgresqlLayer, - constraints: ItemConstraints = None, + constraints: ItemConstraints, ast: Type[Node] = None, ) -> Type[FeatureSetProvider]: filters = ( @@ -414,8 +414,8 @@ async def _get_table_temporal_extents( # noqa: C901 self, table_models: Dict[str, sa.Table], table_temporal_fields: Dict[str, List[TemporalInstant]], - ) -> Dict[str, List[datetime]]: - table_temporal_extents = {} + ) -> Dict[str, List[List[datetime]]]: + table_temporal_extents: Dict[str, List[List[datetime]]] = {} for qualified_table_name, temporal_fields in table_temporal_fields.items(): start = None end = None diff --git a/oaff/app/oaff/app/gateway.py b/oaff/app/oaff/app/gateway.py index 98843fd..ee2d31c 100644 --- a/oaff/app/oaff/app/gateway.py +++ b/oaff/app/oaff/app/gateway.py @@ -7,15 +7,11 @@ from oaff.app.request_handlers.collection_items import ( CollectionsItems as CollectionsItemsRequestHandler, ) -from oaff.app.request_handlers.collections_list import ( - CollectionsList as CollectionsListRequestHandler, -) +from oaff.app.request_handlers.collections_list import CollectionsListRequestHandler from oaff.app.request_handlers.common.request_handler import RequestHandler -from oaff.app.request_handlers.conformance import Conformance as ConformanceRequestHandler +from oaff.app.request_handlers.conformance import ConformanceRequestHandler from oaff.app.request_handlers.feature import Feature as FeatureRequestHandler -from oaff.app.request_handlers.landing_page import ( - LandingPage as LandingPageRequestHandler, -) +from oaff.app.request_handlers.landing_page import LandingPageRequestHandler from oaff.app.requests.common.request_type import RequestType from oaff.app.responses.response import Response diff --git a/oaff/app/oaff/app/request_handlers/collection_items.py b/oaff/app/oaff/app/request_handlers/collection_items.py index 1aac299..a9f9226 100644 --- a/oaff/app/oaff/app/request_handlers/collection_items.py +++ b/oaff/app/oaff/app/request_handlers/collection_items.py @@ -1,5 +1,5 @@ from datetime import datetime, tzinfo -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union +from typing import Any, Callable, Dict, Optional, Tuple, Type, Union, cast from pygeofilter.ast import ( And, @@ -82,7 +82,7 @@ def _get_page_link_retriever( frontend_config = get_frontend_configuration() - def retriever(total_count: int, result_count: int) -> List[Link]: + def retriever(total_count: int, result_count: int) -> Dict[PageLinkRel, Link]: links = {} if request.offset > 0: links[PageLinkRel.PREV] = Link( @@ -107,7 +107,7 @@ def _collect_ast( self, bbox: BBox, datetime: Any, - ) -> Type[Node]: + ) -> Optional[Type[Node]]: if bbox is None and datetime is None: return None else: @@ -120,21 +120,23 @@ def _collect_ast( async def _spatial_bounds_to_node( self, - spatial_bounds: Optional[ - Union[ - Tuple[float, float, float, float], - Tuple[float, float, float, float, float, float], - ] + spatial_bounds: Union[ + Tuple[float, float, float, float], + Tuple[float, float, float, float, float, float], ], spatial_bounds_crs: str, data_source: DataSource, layer: Layer, ) -> BBox: - x_min, y_min, x_max, y_max = ( - spatial_bounds - if len(spatial_bounds) == 4 - else (spatial_bounds[i] for i in [0, 1, 3, 4]) - ) + # recommended usage for Union of types + # https://github.com/python/mypy/issues/1178#issuecomment-176185607 + if len(spatial_bounds) == 4: + a, b, c, d = cast(Tuple[float, float, float, float], spatial_bounds) + else: + a, b, _, c, d, _ = cast( + Tuple[float, float, float, float, float, float], spatial_bounds + ) + x_min, y_min, x_max, y_max = (a, b, c, d) transformer = Transformer.from_crs( "EPSG:4326", # True until Features API spec part 2 is implemented f"{layer.geometry_crs_auth_name}:{layer.geometry_crs_auth_code}", @@ -155,13 +157,13 @@ async def _datetime_to_node( # noqa: C901 self, temporal_bounds: Union[Tuple[datetime], Tuple[datetime, datetime]], layer: Layer, - ) -> Type[Node]: + ) -> Optional[Type[Node]]: if len(list(filter(lambda bound: bound is not None, temporal_bounds))) == 0: return None nodes = [] for data_field in layer.temporal_attributes: if len(temporal_bounds) == 2: - query_start, query_end = temporal_bounds + query_start, query_end = cast(Tuple[datetime, datetime], temporal_bounds) if data_field.__class__ is TemporalInstant: if query_start is not None and query_end is not None: nodes.append( @@ -333,6 +335,6 @@ def _match_query_time_to_end_field( ) def _match_query_time_to( - self, query_time: datetime, tz_aware: bool, tz: Type[tzinfo] + self, query_time: datetime, tz_aware: bool, tz: tzinfo ) -> datetime: return query_time if tz_aware else query_time.astimezone(tz).replace(tzinfo=None) diff --git a/oaff/app/oaff/app/request_handlers/collections_list.py b/oaff/app/oaff/app/request_handlers/collections_list.py index 73cf644..390a3c9 100644 --- a/oaff/app/oaff/app/request_handlers/collections_list.py +++ b/oaff/app/oaff/app/request_handlers/collections_list.py @@ -3,7 +3,7 @@ from oaff.app.configuration.data import get_layers from oaff.app.request_handlers.common.request_handler import RequestHandler -from oaff.app.requests.collections_list import CollectionsList +from oaff.app.requests.collections_list import CollectionsList as CollectionsListRequest from oaff.app.responses.models.collection import CollectionHtml, CollectionJson from oaff.app.responses.models.collections import CollectionsHtml, CollectionsJson from oaff.app.responses.response import Response @@ -12,12 +12,12 @@ LOGGER: Final = getLogger(__file__) -class CollectionsList(RequestHandler): +class CollectionsListRequestHandler(RequestHandler): @classmethod def type_name(cls) -> str: - return CollectionsList.__name__ + return CollectionsListRequest.__name__ - async def handle(self, request: CollectionsList) -> Type[Response]: + async def handle(self, request: CollectionsListRequest) -> Type[Response]: format_links = self.get_links_for_self(request) if request.format == ResponseFormat.html: collections = [ diff --git a/oaff/app/oaff/app/request_handlers/conformance.py b/oaff/app/oaff/app/request_handlers/conformance.py index 32750d9..1f7b406 100644 --- a/oaff/app/oaff/app/request_handlers/conformance.py +++ b/oaff/app/oaff/app/request_handlers/conformance.py @@ -1,18 +1,18 @@ from typing import Type from oaff.app.request_handlers.common.request_handler import RequestHandler -from oaff.app.requests.conformance import Conformance +from oaff.app.requests.conformance import Conformance as ConformanceRequest from oaff.app.responses.models.conformance import ConformanceHtml, ConformanceJson from oaff.app.responses.response import Response from oaff.app.responses.response_format import ResponseFormat -class Conformance(RequestHandler): +class ConformanceRequestHandler(RequestHandler): @classmethod def type_name(cls) -> str: - return Conformance.__name__ + return ConformanceRequest.__name__ - async def handle(self, request: Conformance) -> Type[Response]: + async def handle(self, request: ConformanceRequest) -> Type[Response]: conform_list = [ "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core", "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections", diff --git a/oaff/app/oaff/app/request_handlers/landing_page.py b/oaff/app/oaff/app/request_handlers/landing_page.py index 8138892..3080347 100644 --- a/oaff/app/oaff/app/request_handlers/landing_page.py +++ b/oaff/app/oaff/app/request_handlers/landing_page.py @@ -3,7 +3,7 @@ from oaff.app.configuration.frontend_interface import get_frontend_configuration from oaff.app.i18n.translations import gettext_for_locale from oaff.app.request_handlers.common.request_handler import RequestHandler -from oaff.app.requests.landing_page import LandingPage +from oaff.app.requests.landing_page import LandingPage as LandingPageRequest from oaff.app.responses.models.landing import LandingHtml, LandingJson from oaff.app.responses.models.link import Link, LinkRel from oaff.app.responses.response import Response @@ -12,12 +12,12 @@ from oaff.app.settings import OPENAPI_OGC_TYPE -class LandingPage(RequestHandler): +class LandingPageRequestHandler(RequestHandler): @classmethod def type_name(cls) -> str: - return LandingPage.__name__ + return LandingPageRequest.__name__ - async def handle(self, request: LandingPage) -> Type[Response]: + async def handle(self, request: LandingPageRequest) -> Type[Response]: gettext = gettext_for_locale(request.locale) frontend = get_frontend_configuration() title = gettext("Features API Landing Page") diff --git a/oaff/app/oaff/app/responses/response_format.py b/oaff/app/oaff/app/responses/response_format.py index 0a34fd8..34f954a 100644 --- a/oaff/app/oaff/app/responses/response_format.py +++ b/oaff/app/oaff/app/responses/response_format.py @@ -3,7 +3,7 @@ from oaff.app.responses.response_type import ResponseType -class ResponseFormat(dict, Enum): +class ResponseFormat(dict, Enum): # type: ignore[misc] html = { ResponseType.DATA: "text/html", ResponseType.METADATA: "text/html", diff --git a/oaff/app/oaff/app/responses/templates/templates.py b/oaff/app/oaff/app/responses/templates/templates.py index c8f62ff..2fbbf2e 100644 --- a/oaff/app/oaff/app/responses/templates/templates.py +++ b/oaff/app/oaff/app/responses/templates/templates.py @@ -13,7 +13,7 @@ def get_rendered_html(template_name: str, data: object, locale: Locales) -> str: loader=PackageLoader("oaff.app", path.join("responses", "templates", "html")), autoescape=select_autoescape(["html"]), ) - env.install_gettext_translations(get_translations_for_locale(locale)) + env.install_gettext_translations(get_translations_for_locale(locale)) # type: ignore frontend_config = get_frontend_configuration() return env.get_template(f"{template_name}.jinja2").render( response=data, diff --git a/oaff/fastapi/api/openapi/openapi.py b/oaff/fastapi/api/openapi/openapi.py index cdd8567..7d2368d 100644 --- a/oaff/fastapi/api/openapi/openapi.py +++ b/oaff/fastapi/api/openapi/openapi.py @@ -1,10 +1,12 @@ +from typing import Callable + from fastapi.applications import FastAPI from fastapi.requests import Request from oaff.fastapi.api.openapi.vnd_response import VndResponse -def get_openapi_handler(app: FastAPI) -> VndResponse: +def get_openapi_handler(app: FastAPI) -> Callable[[Request], VndResponse]: def handler(_: Request): # OpenAPI spec must be modified because FastAPI doesn't support # encoding style: https://github.com/tiangolo/fastapi/issues/283 diff --git a/oaff/fastapi/api/routes/collections.py b/oaff/fastapi/api/routes/collections.py index 32509ed..327de74 100644 --- a/oaff/fastapi/api/routes/collections.py +++ b/oaff/fastapi/api/routes/collections.py @@ -1,22 +1,19 @@ from datetime import datetime from logging import getLogger -from typing import Final, Optional, Tuple, Union +from typing import Final, Optional, Tuple, Union, cast from urllib.parse import quote import iso8601 import pytz -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Query from fastapi.param_functions import Depends -from fastapi.params import Query from fastapi.requests import Request from oaff.app.requests.collection import Collection as CollectionRequestType from oaff.app.requests.collection_items import ( CollectionItems as CollectionItemsRequestType, ) -from oaff.app.requests.collections_list import ( - CollectionsList as CollectionsListRequestType, -) +from oaff.app.requests.collections_list import CollectionsList as CollectionsListRequest from oaff.app.requests.feature import Feature as FeatureRequestType from oaff.app.responses.response_type import ResponseType from oaff.fastapi.api import settings @@ -42,7 +39,7 @@ async def get_collections_list( ): enforce_strict(request) return await delegate( - CollectionsListRequestType( + CollectionsListRequest( type=ResponseType.METADATA, format=common_parameters.format, locale=common_parameters.locale, @@ -216,7 +213,7 @@ def parse_datetime(datetime_str: str) -> datetime: raise HTTPException( status_code=400, detail="datetime start cannot be after end" ) - return tuple(result) + return cast(Union[Tuple[datetime], Tuple[datetime, datetime]], tuple(result)) def _get_safe_url(path_template: str, request: Request, root: str) -> str: diff --git a/oaff/fastapi/api/routes/common/common_parameters.py b/oaff/fastapi/api/routes/common/common_parameters.py index 06c1476..eeaac49 100644 --- a/oaff/fastapi/api/routes/common/common_parameters.py +++ b/oaff/fastapi/api/routes/common/common_parameters.py @@ -1,5 +1,5 @@ from re import compile, search, sub -from typing import Final, Optional, Set +from typing import Final, List, Optional from fastapi import Header, Query from fastapi.requests import Request @@ -66,7 +66,7 @@ async def populate( ) @classmethod - def _header_options_by_preference(cls, header_value: str) -> Set[str]: + def _header_options_by_preference(cls, header_value: str) -> List[str]: options = list( filter( lambda option: len(option) > 0, diff --git a/oaff/fastapi/api/routes/common/parameter_control.py b/oaff/fastapi/api/routes/common/parameter_control.py index 9898da1..a81d3a7 100644 --- a/oaff/fastapi/api/routes/common/parameter_control.py +++ b/oaff/fastapi/api/routes/common/parameter_control.py @@ -7,7 +7,8 @@ from oaff.fastapi.api.routes.common.common_parameters import COMMON_QUERY_PARAMS -def strict(request: Request, permitted: Optional[List[str]] = []) -> None: +def strict(request: Request, permitted: Optional[List[str]] = None) -> None: + permitted = list() if permitted is None else permitted excessive = set(request.query_params.keys()).difference( set(permitted + COMMON_QUERY_PARAMS) ) diff --git a/oaff/fastapi/api/routes/conformance.py b/oaff/fastapi/api/routes/conformance.py index 8d14721..01526a7 100644 --- a/oaff/fastapi/api/routes/conformance.py +++ b/oaff/fastapi/api/routes/conformance.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends from fastapi.requests import Request -from oaff.app.requests.conformance import Conformance as ConformanceRequestType +from oaff.app.requests.conformance import Conformance as ConformanceRequest from oaff.app.responses.response_type import ResponseType from oaff.fastapi.api import settings from oaff.fastapi.api.delegator import delegate, get_default_handler @@ -24,7 +24,7 @@ async def root( ): enforce_strict(request) return await delegate( - ConformanceRequestType( + ConformanceRequest( type=ResponseType.METADATA, format=common_parameters.format, url=str(request.url), diff --git a/oaff/fastapi/api/routes/landing_page.py b/oaff/fastapi/api/routes/landing_page.py index 9739141..5f1be71 100644 --- a/oaff/fastapi/api/routes/landing_page.py +++ b/oaff/fastapi/api/routes/landing_page.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends from fastapi.requests import Request -from oaff.app.requests.landing_page import LandingPage as LandingPageRequestType +from oaff.app.requests.landing_page import LandingPage as LandingPageRequest from oaff.app.responses.response_type import ResponseType from oaff.fastapi.api import settings from oaff.fastapi.api.delegator import delegate, get_default_handler @@ -24,7 +24,7 @@ async def root( ): enforce_strict(request) return await delegate( - LandingPageRequestType( + LandingPageRequest( type=ResponseType.METADATA, format=common_parameters.format, url=str(request.url), diff --git a/oaff/fastapi/api/util.py b/oaff/fastapi/api/util.py index 0bc820b..52c3ee2 100644 --- a/oaff/fastapi/api/util.py +++ b/oaff/fastapi/api/util.py @@ -57,8 +57,8 @@ def _change_page(url: str, forward: bool) -> str: for key, value in { **parameters, **{ - "offset": max(offset + limit * (1 if forward else -1), 0), - "limit": limit, + "offset": str(max(offset + limit * (1 if forward else -1), 0)), + "limit": str(limit), }, }.items() ] diff --git a/oaff/fastapi/gunicorn/gunicorn.conf.py b/oaff/fastapi/gunicorn/gunicorn.conf.py index b938fc0..c2eb6ec 100644 --- a/oaff/fastapi/gunicorn/gunicorn.conf.py +++ b/oaff/fastapi/gunicorn/gunicorn.conf.py @@ -2,7 +2,7 @@ cpu_limit = os.environ.get("API_CPU_LIMIT") if cpu_limit is None: - num_avail_cpus = len(os.sched_getaffinity(0)) + num_avail_cpus = len(os.sched_getaffinity(0)) # type: ignore[attr-defined] else: try: num_avail_cpus = int(cpu_limit) diff --git a/oaff/fastapi/tests/test_conformance_delegation.py b/oaff/fastapi/tests/test_conformance_delegation.py index 224e07f..00414d8 100644 --- a/oaff/fastapi/tests/test_conformance_delegation.py +++ b/oaff/fastapi/tests/test_conformance_delegation.py @@ -1,6 +1,6 @@ from typing import Final -from oaff.app.requests.conformance import Conformance +from oaff.app.requests.conformance import Conformance as ConformanceRequest from oaff.fastapi.api.routes.conformance import PATH as ROOT_PATH from oaff.fastapi.tests import common_delegation as common @@ -24,7 +24,7 @@ def test_basic_defaults(test_app): common.test_basic_defaults( test_app, endpoint_path, - Conformance, + ConformanceRequest, handler_calls, ) diff --git a/oaff/fastapi/tests/test_landing_delegation.py b/oaff/fastapi/tests/test_landing_delegation.py index 401b68d..0a94850 100644 --- a/oaff/fastapi/tests/test_landing_delegation.py +++ b/oaff/fastapi/tests/test_landing_delegation.py @@ -1,6 +1,6 @@ from typing import Final -from oaff.app.requests.landing_page import LandingPage +from oaff.app.requests.landing_page import LandingPage as LandingPageRequest from oaff.fastapi.api.routes.landing_page import PATH as ROOT_PATH from oaff.fastapi.tests import common_delegation as common @@ -24,7 +24,7 @@ def test_basic_defaults(test_app): common.test_basic_defaults( test_app, endpoint_path, - LandingPage, + LandingPageRequest, handler_calls, ) diff --git a/pyproject.toml b/pyproject.toml index 70b6854..e3420a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,10 +34,11 @@ sections='FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER' no_lines_before='LOCALFOLDER' [tool.mypy] -exclude="^(tests|testing)$" +exclude="^(tests|testing|data|scripts)$" namespace_packages=true explicit_package_bases=true ignore_missing_imports=true -ignore_errors=true install_types=true -non_interactive=true \ No newline at end of file +non_interactive=true +no_warn_no_return=true +no_strict_optional=true