From 77917efeef6c881fd310a0528f2e34f68c228043 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 30 Aug 2023 14:43:44 -0400 Subject: [PATCH 1/9] support API request paths under GeoServer service (fixes #584) --- CHANGES.rst | 9 ++++- config/providers.cfg | 45 +++++++++++++++++----- docs/services.rst | 15 +++++--- magpie/services.py | 89 +++++++++++++++++++++++++++++--------------- 4 files changed, 112 insertions(+), 46 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8c32b8fea..78cd7f649 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,7 +9,14 @@ Changes `Unreleased `_ (latest) ------------------------------------------------------------------------------------ -* Nothing new for the moment. +Features / Changes +~~~~~~~~~~~~~~~~~~~~~ +* Add support of RESTful API endpoints (i.e.: ``ServiceAPI``) under ``ServiceGeoserver`` using ``Route`` resources + (fixes `#584 `_). + Requires the `Service` to be configured either with the default ``configuration``, + or by explicitly setting ``api: true``. When a HTTP request is sent toward a `Service` typed ``ServiceGeoserver``, + any non-`OWS` request (i.e.: `WFS`, `WMS`, `WPS`) will default to the resolution handling of typical ``ServiceAPI``. + This can be used notably to access the ``/web`` and ``/ogc`` endpoints of a `GeoServer` instance. .. _changes_3.34.0: diff --git a/config/providers.cfg b/config/providers.cfg index dd6482ea8..66ef921d5 100644 --- a/config/providers.cfg +++ b/config/providers.cfg @@ -10,15 +10,42 @@ # # Parameters: # ----------- -# url: private URL of the service to be created -# title: pretty name of the service (real name is the section key) -# public: parameter passed down to Phoenix for service registration -# c4i: parameter passed down to Phoenix for service registration -# type: service type to use for creation, must be one of the known Magpie service types -# (see: magpie.services.SERVICE_TYPE_DICT) -# sync_type: service synchronization type, must be one of the known Magpie service sync-types, -# often equals to 'type' (see: magpie.cli.SYNC_SERVICES_TYPES) -# hooks: list of request processing hooks for the service +# url: private URL of the service to be created +# title: pretty name of the service (real name is the section key) +# public: parameter passed down to Phoenix for service registration +# c4i: parameter passed down to Phoenix for service registration +# type: service type to use for creation, must be one of the known Magpie service types +# (see: magpie.services.SERVICE_TYPE_DICT) +# sync_type: service synchronization type, must be one of the known Magpie service sync-types, +# often equals to 'type' (see: magpie.cli.sync_services.SYNC_SERVICES_TYPES) +# configuration: advanced custom configuration for service type that support it (see details in section below) +# hooks: list of request processing hooks for the service (see details in section below) +# +# Configuration (requires Magpie>=3.21.0 minimally, or more recent version as relevant for specific features) +# ------------- +# Some services allow custom configuration settings to slightly modify their behaviour for handling requests. +# This can be used to enable/disable a subset of functionalities offered by a given service, or to control special +# logic of request properties for the service. The specific configuration format differs for each service type, and +# is only supported for certain cases listed below. More configuration details are provided in the documentation. +# +# - type: geoserver (see also: https://pavics-magpie.readthedocs.io/en/latest/services.html#servicegeoserver) +# configuration: +# wfs: true|false # allows use of OWS WFS requests with Workspaces and Layers +# wms: true|false # allows use of OWS WMS requests with Workspaces and Layers +# wps: true|false # allows use of OWS WPS requests with Workspaces and Processes +# api: true|false # allows use of REST API requests with OGC API and Web UI endpoints +# +# - type: thredds (see also: https://pavics-magpie.readthedocs.io/en/latest/services.html#servicethredds) +# configuration: +# skip_prefix: "/" # path prefix to skip (strip) before processing the rest of the request path +# file_patterns: # patterns to map different path variations to a same file resource +# - "" +# metadata_type: # path prefix to resources to consider as BROWSE-able metadata +# prefixes: +# - "" +# data_type: # path prefix to resources to consider as READ-able data +# prefixes: +# - "" # # Hooks: (requires Magpie>=3.25.0, Twitcher>=0.7.0) # ------ diff --git a/docs/services.rst b/docs/services.rst index bfe06374f..5ad1cf9c7 100644 --- a/docs/services.rst +++ b/docs/services.rst @@ -571,16 +571,18 @@ administrator intends to only make use (for the moment) of :term:`WFS` functiona url: http://localhost:1234 type: geoserver - # customizable configuration (enable desired OWS request handlers) - # all OWS are enabled by default if no configuration is provided + # customizable configuration (enable desired OWS/REST request handlers) + # all OWS/REST services are enabled by default if no configuration is provided configuration: wfs: true wms: false wps: false + api: false -This would make sure that request parsing and access to :term:`WMS` and :term:`WPS` endpoints is disabled, but leaves -the :term:`Resource` definitions available for use at a later time if the administrator decides to eventually make use -of them. For example, the administrator could decide to start using :term:`WMS` as well without any further change +This would make sure that request parsing and access to :term:`WMS`, :term:`WPS` and any REST :term:`API` endpoints +are disabled, but leaves the :term:`Resource` definitions available for use at a later time if the administrator +decides to eventually make use of them. +For example, the administrator could decide to start using :term:`WMS` as well without any further change needed other than updating this :term:`Service` custom configuration and applying :term:`Permissions ` specific only to :term:`WMS`. All other :term:`Applied Permissions ` to existing :term:`User`, :term:`Group` and :term:`Resource` @@ -590,7 +592,8 @@ the :term:`WFS` to :term:`WMS` request handlers. .. note:: Custom configuration can be provided With either the `providers.cfg`_ (as presented above), in a :ref:`config_file` as described in greater lengths within the :ref:`configuration` chapter, - or by providing the ``configuration`` field directly within the API request body during :term:`Service` creation. + or by providing the ``configuration`` field directly within the :term:`API` request body during + :term:`Service` creation. Service Synchronization diff --git a/magpie/services.py b/magpie/services.py index ecea96cab..190d565b7 100644 --- a/magpie/services.py +++ b/magpie/services.py @@ -439,7 +439,7 @@ def _get_connected_object(self, obj): def _get_request_path_parts(self): # type: () -> Optional[List[Str]] """ - Obtain the :attr:`request` path parts striped of anything prior to the referenced :attr:`service` name. + Obtain the :attr:`request` path parts stripped of anything prior to the referenced :attr:`service` name. """ path_parts = self.request.path.rstrip("/").split("/") svc_name = self.service.resource_name @@ -728,15 +728,6 @@ def _set_request(self, request): request = property(_get_request, _set_request) - @property - @abc.abstractmethod - def service_base(self): - # type: () -> Str - """ - Name of the base :term:`OWS` functionality serviced by `Geoserver`. - """ - raise NotImplementedError - @abc.abstractmethod def resource_requested(self): # type: () -> MultiResourceRequested @@ -766,7 +757,6 @@ class ServiceWPS(ServiceOWS): """ Service that represents a ``Web Processing Service`` endpoint. """ - service_base = "wps" service_type = "wps" permissions = [ @@ -843,7 +833,6 @@ class ServiceNCWMS2(ServiceBaseWMS): .. seealso:: https://reading-escience-centre.gitbooks.io/ncwms-user-guide/content/04-usage.html """ - service_base = "wms" service_type = "ncwms" permissions = [ @@ -929,6 +918,15 @@ class ServiceGeoserverBase(ServiceOWS): Provides basic configuration parameters and functionalities shared by `Geoserver` implementations. """ + @property + @abc.abstractmethod + def service_base(self): + # type: () -> Str + """ + Name of the base :term:`OWS` functionality serviced by `Geoserver`. + """ + raise NotImplementedError + @classmethod @classproperty @abc.abstractmethod @@ -1004,7 +1002,9 @@ def params_expected(cls): # noqa # pylint: disable=E0213,no-self-argument,W022 The :attr:`resource_param` is also added to ensure it is always parsed based on the derived implementation. """ - if isinstance(cls.resource_param, six.string_types): + if cls.resource_param is None: + return [] + elif isinstance(cls.resource_param, six.string_types): impl_params = [cls.resource_param] elif isinstance(cls.resource_param, list): impl_params = cls.resource_param @@ -1141,7 +1141,7 @@ def resource_requested(self): class ServiceGeoserverWMS(ServiceGeoserverBase, ServiceBaseWMS): # order important to call overridden class properties """ - Service that represents a ``Web Map Service`` endpoint with functionalities specific to ``GeoServer``. + Service that represents a `Web Map Service` endpoint with functionalities specific to `Geoserver`. .. seealso:: https://docs.geoserver.org/latest/en/user/services/wms/reference.html @@ -1239,12 +1239,11 @@ def permission_requested(self): class ServiceWFS(ServiceOWS): """ - Service that represents a ``Web Feature Service`` endpoint. + Service that represents a `Web Feature Service` endpoint. .. seealso:: https://www.ogc.org/standards/wfs (OpenGIS WFS 2.0.0 implementation) """ - service_base = "wfs" service_type = "wfs" permissions = [ @@ -1298,7 +1297,7 @@ def resource_requested(self): class ServiceGeoserverWFS(ServiceGeoserverBase, ServiceWFS): # order important to call overridden class properties """ - Service that represents a ``Web Feature Service`` endpoint with functionalities specific to ``GeoServer``. + Service that represents a `Web Feature Service` endpoint with functionalities specific to `Geoserver`. .. seealso:: https://docs.geoserver.org/latest/en/user/services/wfs/reference.html @@ -1332,7 +1331,7 @@ class ServiceGeoserverWFS(ServiceGeoserverBase, ServiceWFS): # order important class ServiceTHREDDS(ServiceInterface): """ - Service that represents a ``THREDDS Data Server`` endpoint. + Service that represents a `THREDDS Data Server` endpoint. """ service_type = "thredds" @@ -1451,8 +1450,9 @@ def permission_requested(self): class ServiceGeoserverWPS(ServiceGeoserverBase, ServiceWPS): # order important to call overridden class properties """ - Service that represents a ``Web Processing Service`` under a `Geoserver` instance. + Service that represents a `Web Processing Service` under a `Geoserver` instance. """ + service_base = "wps" service_type = "geoserverwps" resource_scoped = False # name in 'identifier' must not be split and does not match WORKSPACE @@ -1472,9 +1472,22 @@ class ServiceGeoserverWPS(ServiceGeoserverBase, ServiceWPS): # order important } +class ServiceGeoserverAPI(ServiceAPI, ServiceGeoserverBase): + """ + Service that represents a generic `REST API` under a `Geoserver` instance. + """ + service_base = "api" + service_type = "geoserverapi" + + resource_scoped = False + resource_multi = False + resource_param = None + resource_types_permissions = ServiceAPI.resource_types_permissions + + class ServiceGeoserver(ServiceGeoserverBase): """ - Service that encapsulates the multiple :term:`OWS` endpoints from `GeoServer` services. + Service that encapsulates the multiple :term:`OWS` endpoints from `Geoserver` services. .. seealso:: https://docs.geoserver.org/stable/en/user/services/index.html @@ -1485,17 +1498,20 @@ class ServiceGeoserver(ServiceGeoserverBase): ServiceGeoserverWFS.service_base: ServiceGeoserverWFS, ServiceGeoserverWMS.service_base: ServiceGeoserverWMS, ServiceGeoserverWPS.service_base: ServiceGeoserverWPS, + ServiceGeoserverAPI.service_base: ServiceGeoserverAPI, } @classproperty - def service_ows_supported(cls): # noqa # pylint: disable=E0213,no-self-argument,W0221,arguments-differ - # type: () -> Set[Type[ServiceOWS]] + def service_supported(cls): # noqa # pylint: disable=E0213,no-self-argument,W0221,arguments-differ + # type: () -> Set[Type[ServiceGeoserverBase]] return set(cls.service_map.values()) # only allow workspace directly under service # then, only layer or process under that workspace + # allow nesting any amount of routes to form a prefix, which then can lead to an OWS endpoint, or any other REST API child_structure_allowed = { - models.Service: [models.Workspace], + models.Service: [models.Workspace, models.Route], + models.Route: [models.Workspace, models.Route], models.Workspace: [models.Layer, models.Process], models.Layer: [], models.Process: [], @@ -1511,6 +1527,14 @@ def service_ows_supported(cls): # noqa # pylint: disable=E0213,no-self-argumen Without the :class:`models.Workspace` scope in the path, ``identifier`` parameter fails to be resolved by `Geoserver`, as if it was unspecified. Attribute :attr:`ServiceGeoserverWPS.resource_scoped` controls the behaviour of splitting the defined :attr:`resource_param` into :class:`models.Workspace` and child components. + + .. note:: + The :class:`models.Route` is allowed at the root of the service and for any nested :class:`models.Route` + resource to support various endpoints such as the ``/web`` user interface, or the REST interface for the + new generation of `OGC API` services typically on ``/ogc/{features|maps|processes}`` endpoints. No special + logic is applied for the different services nested under those endpoints. All of them are handled as *typical* + RESTful APIs, for which permissions and appropriate sub-paths should be defined accordingly with their + respective specifications and schema. """ configurable = True @@ -1536,7 +1560,7 @@ def get_config(self): def params_expected(cls): # noqa # pylint: disable=E0213,no-self-argument,W0221,arguments-differ # type: () -> List[Str] params = set() - for svc in cls.service_ows_supported: + for svc in cls.service_supported: if issubclass(svc, ServiceOWS): param_names = getattr(svc, "params_expected", None) if param_names: @@ -1547,7 +1571,7 @@ def params_expected(cls): # noqa # pylint: disable=E0213,no-self-argument,W022 def permissions(cls): # noqa # pylint: disable=E0213,no-self-argument,W0221,arguments-differ # type: () -> List[Permission] perms = set() - for svc in cls.service_ows_supported: + for svc in cls.service_supported: if issubclass(svc, ServiceOWS): svc_perms = getattr(svc, "permissions", None) if svc_perms: @@ -1558,7 +1582,7 @@ def permissions(cls): # noqa # pylint: disable=E0213,no-self-argument,W0221,ar def resource_types_permissions(cls): # noqa # pylint: disable=E0213,no-self-argument,W0221,arguments-differ # type: () -> ResourceTypePermissions perms = {} # type: ResourceTypePermissions - for svc in cls.service_ows_supported: + for svc in cls.service_supported: if issubclass(svc, ServiceOWS): svc_res_perms = getattr(svc, "resource_types_permissions", None) if svc_res_perms: @@ -1591,17 +1615,20 @@ def service_requested(self): req = self.parser.params["request"] if not svc and req: # geoserver allows omitting 'service' request query parameter because it can be inferred from the path - # since all OWS services are accessed using '/geoserver/?request=...' + # since all OWS services can also be accessed using '/geoserver/?request=...' # attempt to match using applicable path fragment svc_path = self.request.path.rsplit("/", 1)[-1].lower() - for svc_ows in type(self).service_ows_supported: # pylint: disable=E1133,not-an-iterable + for svc_ows in type(self).service_supported: # pylint: disable=E1133,not-an-iterable if svc_path == svc_ows.service_base: svc = svc_ows.service_base break config = self.get_config() if svc not in config or not config[svc]: - self._service_requested = None - return None + if config.get(ServiceGeoserverAPI.service_base): + self._service_requested = ServiceGeoserverAPI + else: + self._service_requested = None + return self._service_requested self._service_requested = type(self).service_map[svc] return self._service_requested @@ -1645,6 +1672,8 @@ def permission_requested(self): ServiceGeoserverWFS, ServiceGeoserverWMS, ServiceGeoserverWPS, + # purposely omit 'ServiceGeoserverAPI' since it is just like base 'ServiceAPI' on its own + # no value to duplicate it outside its use within 'ServiceGeoserver' ServiceNCWMS2, ServiceTHREDDS, ServiceWFS, From 4ea3cbc19d02f7c4bbdfe764b259949c75d4f3cb Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 30 Aug 2023 15:16:23 -0400 Subject: [PATCH 2/9] fixes to UI pages for rendering applicable permissions when many are listed --- CHANGES.rst | 9 +++++- magpie/ui/home/static/style.css | 13 ++++++++ .../ui/management/templates/edit_group.mako | 12 ++++---- .../ui/management/templates/edit_service.mako | 30 +++++++++---------- magpie/ui/management/templates/edit_user.mako | 16 +++++----- .../templates/view_pending_user.mako | 16 +++++----- magpie/ui/management/views.py | 15 +++++----- .../ui/user/templates/edit_current_user.mako | 16 +++++----- 8 files changed, 74 insertions(+), 53 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 78cd7f649..51d22954d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -.. explicit references must be used in this file (not references.rst) to ensure they are directly rendered on Github + .. explicit references must be used in this file (not references.rst) to ensure they are directly rendered on Github .. :changelog: Changes @@ -18,6 +18,13 @@ Features / Changes any non-`OWS` request (i.e.: `WFS`, `WMS`, `WPS`) will default to the resolution handling of typical ``ServiceAPI``. This can be used notably to access the ``/web`` and ``/ogc`` endpoints of a `GeoServer` instance. +Bug Fixes +~~~~~~~~~~~~~~~~~~~~~ +* Fix `UI` rendering of the `Permission` label list under a `Service` edition page when a large amount of possible + permissions is applicable. This was notably the case of ``ServiceGeoserver`` that combines permissions of multiple + `OWS`-based services, which where going out of bound of the UI page. +* Fix `UI` scrollbars going over the `Permission` titles in the `User` and `Group` permission edition pages. + .. _changes_3.34.0: `3.34.0 `_ (2023-05-31) diff --git a/magpie/ui/home/static/style.css b/magpie/ui/home/static/style.css index 42cf12699..b8523d9ff 100644 --- a/magpie/ui/home/static/style.css +++ b/magpie/ui/home/static/style.css @@ -388,6 +388,12 @@ table.panel-line td { max-width: 1em; } +.panel-line-permissions { + max-width: 100%; + white-space: normal; + line-height: 1.5em; +} + .panel-line-checkbox { padding-top: 0.1em; display: inline-block; @@ -400,6 +406,11 @@ table.panel-line td { .panel-entry { font-weight: bold; margin-right: 0.5em; + margin-top: 0.25em; +} + +.panel-entry-title { + vertical-align: top; } .panel-value { @@ -912,6 +923,7 @@ table.simple-list td:last-child { .tree-line-scroll-visible { overflow-x: auto; + min-height: 2.5em; /* ensure the scroll-bar doesn't hide the title texts */ } .tree-key { @@ -977,6 +989,7 @@ div.tree-button { text-overflow: ellipsis; overflow: auto; text-align: left; + min-height: 2em; /* ensure the scroll-bar doesn't hide the title texts */ } .permission-cell { diff --git a/magpie/ui/management/templates/edit_group.mako b/magpie/ui/management/templates/edit_group.mako index cef26a9d4..0e636e770 100644 --- a/magpie/ui/management/templates/edit_group.mako +++ b/magpie/ui/management/templates/edit_group.mako @@ -55,8 +55,8 @@
- - -
- Name: + +
Name:
@@ -97,8 +97,8 @@
- Description: + +
Description:
@@ -135,8 +135,8 @@
- Discoverable: + +
Discoverable:
diff --git a/magpie/ui/management/templates/edit_service.mako b/magpie/ui/management/templates/edit_service.mako index 4f0146f7a..786b6dd64 100644 --- a/magpie/ui/management/templates/edit_service.mako +++ b/magpie/ui/management/templates/edit_service.mako @@ -82,8 +82,8 @@
- - - - - - -
- Name: + +
Name:
@@ -106,8 +106,8 @@
- Protected URL: + +
Protected URL:
@@ -130,8 +130,8 @@
- Public URL: + +
Public URL:
@@ -140,8 +140,8 @@
- Type: + +
Type:
@@ -150,8 +150,8 @@
- Sync Type: + +
Sync Type:
@@ -162,11 +162,11 @@
- Permissions: + +
Permissions:
-
+
%for perm in service_perm: ${perm} %endfor @@ -174,8 +174,8 @@
- ID: + +
ID:
diff --git a/magpie/ui/management/templates/edit_user.mako b/magpie/ui/management/templates/edit_user.mako index e361ec240..399919b77 100644 --- a/magpie/ui/management/templates/edit_user.mako +++ b/magpie/ui/management/templates/edit_user.mako @@ -55,8 +55,8 @@
- - - -
- Username: + +
Username:
%if user_name not in MAGPIE_FIXED_USERS: @@ -107,8 +107,8 @@
- Password: + +
Password:
%if user_name not in MAGPIE_USER_PWD_DISABLED: @@ -159,8 +159,8 @@
- Email: + +
Email:
%if user_name not in MAGPIE_FIXED_USERS: @@ -210,8 +210,8 @@
- Status: + +
Status:
diff --git a/magpie/ui/management/templates/view_pending_user.mako b/magpie/ui/management/templates/view_pending_user.mako index a8213bcf2..f319158d9 100644 --- a/magpie/ui/management/templates/view_pending_user.mako +++ b/magpie/ui/management/templates/view_pending_user.mako @@ -39,8 +39,8 @@
- - -
- Username: + +
Username:
@@ -51,8 +51,8 @@
- Email: + +
Email:
@@ -68,8 +68,8 @@
- Status: + +
Status:
@@ -92,8 +92,8 @@ -
- Registration: + +
Registration:
diff --git a/magpie/ui/management/views.py b/magpie/ui/management/views.py index a8945de37..de9dedec8 100644 --- a/magpie/ui/management/views.py +++ b/magpie/ui/management/views.py @@ -20,7 +20,8 @@ from magpie import register from magpie.api import schemas -from magpie.cli import sync_resources +from magpie.cli.sync_resources import OUT_OF_SYNC, fetch_single_service, get_last_sync, merge_local_and_remote_resources +from magpie.cli.sync_services import SYNC_SERVICES_TYPES from magpie.constants import get_constant # FIXME: remove (REMOTE_RESOURCE_TREE_SERVICE, RESOURCE_TYPE_DICT), implement getters via API from magpie.models import REMOTE_RESOURCE_TREE_SERVICE, RESOURCE_TYPE_DICT, UserGroupStatus, UserStatuses @@ -285,7 +286,7 @@ def edit_user(self): raise HTTPBadRequest(detail=repr(exc)) sync_types = [s["service_sync_type"] for s in services.values()] - sync_implemented = any(s in sync_resources.SYNC_SERVICES_TYPES for s in sync_types) + sync_implemented = any(s in SYNC_SERVICES_TYPES for s in sync_types) info = self.get_remote_resources_info(res_perms, services, session) res_perms, ids_to_clean, last_sync_humanized, out_of_sync = info @@ -648,7 +649,7 @@ def edit_group(self): raise HTTPBadRequest(detail=repr(exc)) sync_types = [s["service_sync_type"] for s in services.values()] - sync_implemented = any(s in sync_resources.SYNC_SERVICES_TYPES for s in sync_types) + sync_implemented = any(s in SYNC_SERVICES_TYPES for s in sync_types) info = self.get_remote_resources_info(res_perms, services, session) res_perms, ids_to_clean, last_sync_humanized, out_of_sync = info @@ -696,7 +697,7 @@ def sync_services(self, services): session = self.request.db for service_info in services.values(): try: - sync_resources.fetch_single_service(service_info["resource_id"], session) + fetch_single_service(service_info["resource_id"], session) transaction.commit() except Exception: # noqa: W0703 # nosec: B110 errors.append(service_info["service_name"]) @@ -720,7 +721,7 @@ def get_remote_resources_info(self, res_perms, services, session): for last_sync, service_name in zip(last_sync_datetimes, services): if last_sync: ids_to_clean += self.get_ids_to_clean(res_perms[service_name]["children"]) - if now - last_sync > sync_resources.OUT_OF_SYNC: + if now - last_sync > OUT_OF_SYNC: out_of_sync.append(service_name) return res_perms, ids_to_clean, last_sync_humanized, out_of_sync @@ -729,7 +730,7 @@ def merge_remote_resources(res_perms, services, session): merged_resources = {} for service_name, service_values in services.items(): service_id = service_values["resource_id"] - merge = sync_resources.merge_local_and_remote_resources + merge = merge_local_and_remote_resources # create a subset for the current local service resources tree # avoids over-copying/looping the multi-service tree by merge function that works on the full set each time local_svc_res = {service_name: res_perms[service_name]} @@ -740,7 +741,7 @@ def merge_remote_resources(res_perms, services, session): @staticmethod def get_last_sync_datetimes(service_ids, session): # type: (List[int], Session) -> List[Optional[datetime]] - return [sync_resources.get_last_sync(s, session) for s in service_ids] + return [get_last_sync(s, session) for s in service_ids] def delete_resource(self, res_id): try: diff --git a/magpie/ui/user/templates/edit_current_user.mako b/magpie/ui/user/templates/edit_current_user.mako index 9001fb557..f08cad82d 100644 --- a/magpie/ui/user/templates/edit_current_user.mako +++ b/magpie/ui/user/templates/edit_current_user.mako @@ -81,8 +81,8 @@
- - - -
- Username: + +
Username:
@@ -105,8 +105,8 @@
- Password: + +
Password:
@@ -156,8 +156,8 @@
- Email: + +
Email:
%if user_edit_email: @@ -216,8 +216,8 @@
- Status: + +
Status:
From 12e8a5c9057d057b98281521032be41881a1d4ea Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 30 Aug 2023 15:55:18 -0400 Subject: [PATCH 3/9] [wip] add tests for ServiceGeoserver with API route support --- config/providers.cfg | 2 +- tests/interfaces.py | 33 ++++++++++++++----- tests/test_services.py | 75 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 93 insertions(+), 17 deletions(-) diff --git a/config/providers.cfg b/config/providers.cfg index 66ef921d5..519d38054 100644 --- a/config/providers.cfg +++ b/config/providers.cfg @@ -139,7 +139,7 @@ providers: title: geoserver public: true c4i: false - type: wfs + type: geoserver sync_type: wfs geoserver-web: diff --git a/tests/interfaces.py b/tests/interfaces.py index 2722bfaf3..0a0b86363 100644 --- a/tests/interfaces.py +++ b/tests/interfaces.py @@ -6902,19 +6902,34 @@ def test_GetResourceTypes_ServiceGeoserver(self): override_resource_types=res_types ) - # nested file/dirs always allowed at any level expect under File - res_types_allowed = [Layer.resource_type_name, Process.resource_type_name, Workspace.resource_type_name] - struct_allowed = { - Service.resource_type_name: [Workspace.resource_type_name], - Workspace.resource_type_name: [Layer.resource_type_name, Process.resource_type_name], - Layer.resource_type_name: [], - Process.resource_type_name: [], - } + if TestVersion(self.version) >= TestVersion("3.35.0"): + res_types_allowed = [ + Route.resource_type_name, + Layer.resource_type_name, + Process.resource_type_name, + Workspace.resource_type_name, + ] + struct_allowed = { + Service.resource_type_name: [Workspace.resource_type_name, Route.resource_type_name], + Route.resource_type_name: [Workspace.resource_type_name, Route.resource_type_name], + Workspace.resource_type_name: [Layer.resource_type_name, Process.resource_type_name], + Layer.resource_type_name: [], + Process.resource_type_name: [], + } + else: + # nested file/dirs always allowed at any level expect under File + res_types_allowed = [Layer.resource_type_name, Process.resource_type_name, Workspace.resource_type_name] + struct_allowed = { + Service.resource_type_name: [Workspace.resource_type_name], + Workspace.resource_type_name: [Layer.resource_type_name, Process.resource_type_name], + Layer.resource_type_name: [], + Process.resource_type_name: [], + } path = "/services/{}".format(self.test_service_name) resp = utils.test_request(self, "GET", path, headers=self.json_headers, cookies=self.cookies) body = utils.check_response_basic_info(resp) - svc = body["service"] + svc = body["service"] # type: JSON utils.check_val_is_in("resource_child_allowed", svc) utils.check_val_is_in("resource_types_allowed", svc) utils.check_val_is_in("resource_structure_allowed", svc) diff --git a/tests/test_services.py b/tests/test_services.py index 91abdf010..b0d29e83d 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -1010,6 +1010,8 @@ def test_ServiceGeoserver_effective_permissions(self): gi: GetFeatureInfo permission (WMS Layer) gm: GetMap (WMS Layer) dp: DescribeProcess permission (WPS Process) + r: Read permission (API Route) + w: Write permission (API Route) A: allow D: deny M: match (makes sense only on layer for any permission except GetCapabilities on service itself) @@ -1031,6 +1033,10 @@ def test_ServiceGeoserver_effective_permissions(self): Layer21 (gf-D-M) dp-D, gf-D, gi-D (revoke access, user > group) Layer22 (gf-D-M) dp-D, gf-D, gi-D (revoke access, both groups) Layer23 (gf-A-M) (gf-D-M) dp-D, gf-A, gi-D (allowed access, user > group) + Route1 r-D, w-D (denied default) + Route2 (r-A-R) r-A, w-D + Route3 (r-A-R) (w-A-R) r-A, w-A + Route4 (w-D-M) r-A, w-D (revoked access) .. note:: Permissions that do not applied to a given sub-:term:`OWS` implementation are automatically denied. @@ -1052,6 +1058,10 @@ def test_ServiceGeoserver_effective_permissions(self): p12_name = "process12" p13_name = "process13" p14_name = "process14" + r1_name = "route1" + r2_name = "route2" + r3_name = "route3" + r4_name = "route4" utils.TestSetup.delete_TestService(self, svc1_name) svc1_id, w1_id = utils.TestSetup.create_TestServiceResourceTree( @@ -1122,6 +1132,34 @@ def test_ServiceGeoserver_effective_permissions(self): override_resource_type=models.Layer.resource_type_name ) l23_id = utils.TestSetup.get_ResourceInfo(self, info)["resource_id"] + info = utils.TestSetup.create_TestResource( + self, + parent_resource_id=svc1_id, + override_resource_name=r1_name, + override_resource_type=models.Route.resource_type_name + ) + r1_id = utils.TestSetup.get_ResourceInfo(self, info)["resource_id"] + info = utils.TestSetup.create_TestResource( + self, + parent_resource_id=r1_id, + override_resource_name=r2_name, + override_resource_type=models.Route.resource_type_name + ) + r2_id = utils.TestSetup.get_ResourceInfo(self, info)["resource_id"] + info = utils.TestSetup.create_TestResource( + self, + parent_resource_id=svc1_id, + override_resource_name=r3_name, + override_resource_type=models.Route.resource_type_name + ) + r3_id = utils.TestSetup.get_ResourceInfo(self, info)["resource_id"] + info = utils.TestSetup.create_TestResource( + self, + parent_resource_id=r3_id, + override_resource_name=r4_name, + override_resource_type=models.Route.resource_type_name + ) + r4_id = utils.TestSetup.get_ResourceInfo(self, info)["resource_id"] # create permissions gcAR = PermissionSet(Permission.GET_CAPABILITIES, Access.ALLOW, Scope.RECURSIVE) # noqa @@ -1134,6 +1172,9 @@ def test_ServiceGeoserver_effective_permissions(self): gmDR = PermissionSet(Permission.GET_MAP, Access.DENY, Scope.RECURSIVE) # noqa dpAR = PermissionSet(Permission.DESCRIBE_PROCESS, Access.ALLOW, Scope.RECURSIVE) # noqa dpDM = PermissionSet(Permission.DESCRIBE_PROCESS, Access.DENY, Scope.MATCH) # noqa + rAR = PermissionSet(Permission.READ, Access.ALLOW, Scope.RECURSIVE) # noqa + wAR = PermissionSet(Permission.WRITE, Access.ALLOW, Scope.RECURSIVE) # noqa + wDM = PermissionSet(Permission.WRITE, Access.DENY, Scope.MATCH) # noqa utils.TestSetup.create_TestUserResourcePermission(self, override_resource_id=svc1_id, override_permission=gcAR) utils.TestSetup.create_TestGroupResourcePermission(self, override_resource_id=svc1_id, override_permission=gmDR) utils.TestSetup.create_TestGroupResourcePermission(self, override_resource_id=w1_id, override_permission=gfAR) @@ -1149,6 +1190,10 @@ def test_ServiceGeoserver_effective_permissions(self): utils.TestSetup.create_TestGroupResourcePermission(self, override_resource_id=l22_id, override_permission=gfDM) utils.TestSetup.create_TestUserResourcePermission(self, override_resource_id=l23_id, override_permission=gfAM) utils.TestSetup.create_TestGroupResourcePermission(self, override_resource_id=l23_id, override_permission=gfDM) + utils.TestSetup.create_TestUserResourcePermission(self, override_resource_id=r2_id, override_permission=rAR) + utils.TestSetup.create_TestUserResourcePermission(self, override_resource_id=r3_id, override_permission=rAR) + utils.TestSetup.create_TestGroupResourcePermission(self, override_resource_id=r3_id, override_permission=wAR) + utils.TestSetup.create_TestGroupResourcePermission(self, override_resource_id=r4_id, override_permission=wDM) # login test user for which the permissions were set self.login_test_user() @@ -1166,9 +1211,9 @@ def _scope(workspace, layer): # type: (Str, Str) -> Str return "{}:{}".format(workspace, layer) - def _test(_path, _params, allow): - # type: (Str, Dict[Str, Str], bool) -> None - req = self.mock_request(_path, method="GET", params=_params) + def _test(_path, _params, allow, method="GET"): + # type: (Str, Dict[Str, Str], bool, Str) -> None + req = self.mock_request(_path, method=method, params=_params) if allow: utils.check_no_raise(lambda: self.ows.check_request(req), msg=_msg(_path, _params)) else: @@ -1205,6 +1250,22 @@ def _test(_path, _params, allow): w2_l2 = _scope(w2_name, l22_name) w2_l3 = _scope(w2_name, l23_name) + # API endpoints only valid with exact paths + r1_path = "{}/{}".format(svc_path, r1_name) + r2_path = "{}/{}".format(svc_path, r2_name) + r3_path = "{}/{}".format(svc_path, r3_name) + r4_path = "{}/{}".format(svc_path, r4_name) + _test(r1_path, {}, method="GET", allow=False) + _test(r2_path, {}, method="GET", allow=True) + _test(r3_path, {}, method="GET", allow=True) + _test(r4_path, {}, method="GET", allow=True) + _test(r1_path, {}, method="POST", allow=False) + _test(r2_path, {}, method="POST", allow=False) + _test(r3_path, {}, method="POST", allow=True) + _test(r4_path, {}, method="POST", allow=False) + _test("/random", {}, method="GET", allow=False) + _test("/random", {}, method="POST", allow=False) + # Layer1, mismatching permission for WMS _test(svc_wms_path, {"request": Permission.GET_FEATURE.title, "layers": w1_l1}, allow=False) _test(w1_wms_path, {"request": Permission.GET_FEATURE.title, "layers": w1_l1}, allow=False) @@ -1402,13 +1463,13 @@ def _add_both(value): layer_permutes = itertools.permutations([w1_l1, w1_l2, w1_l3, w2_l1, w2_l2, w2_l3], quantity) for layer_combo in layer_permutes: query = ",".join(layer_combo) - allow = not any(layer_deny in layer_combo for layer_deny in [w1_l2, w2_l1, w2_l2]) - _test(svc_wfs_path, {"request": Permission.GET_FEATURE.title, "typeNames": query}, allow=allow) + allow_layer = not any(layer_deny in layer_combo for layer_deny in [w1_l2, w2_l1, w2_l2]) + _test(svc_wfs_path, {"request": Permission.GET_FEATURE.title, "typeNames": query}, allow=allow_layer) # multiple layers nested under same workspace will work for request with Workspace isolated path # otherwise, automatic deny regardless if they were allowed during request without workspace in path - allow_w1 = allow and all(layer.startswith(w1_name) for layer in layer_combo) - allow_w2 = allow and all(layer.startswith(w2_name) for layer in layer_combo) + allow_w1 = allow_layer and all(layer.startswith(w1_name) for layer in layer_combo) + allow_w2 = allow_layer and all(layer.startswith(w2_name) for layer in layer_combo) _test(w1_wfs_path, {"request": Permission.GET_FEATURE.title, "typeNames": query}, allow=allow_w1) _test(w2_wfs_path, {"request": Permission.GET_FEATURE.title, "typeNames": query}, allow=allow_w2) From 4ca3ebc08ab2b19f8e8e7791b48c5ce6ce247d99 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 30 Aug 2023 19:31:43 -0400 Subject: [PATCH 4/9] linting fixes --- .readthedocs.yml | 3 +-- Makefile | 5 +++++ magpie/owsrequest.py | 2 -- magpie/services.py | 4 ++-- tests/test_adapter.py | 1 - 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 4f2190ef6..960291ced 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -11,7 +11,7 @@ formats: #- pdf #- epub python: - version: "3.7" + version: "3.10" install: - requirements: requirements-sys.txt - requirements: requirements-doc.txt @@ -21,4 +21,3 @@ python: # path: . # extra_requirements: # - docs - system_packages: true diff --git a/Makefile b/Makefile index 7c74ffc30..1611d22d3 100644 --- a/Makefile +++ b/Makefile @@ -564,12 +564,17 @@ check-security-code-only: mkdir-reports ## run security checks on source code .PHONY: check-docs-only check-docs-only: check-doc8-only check-docf-only check-links-only ## run every code documentation checks +# FIXME: temporary workaround (https://github.com/PyCQA/doc8/issues/145) +# configuration somehow not picked up directly from setup.cfg +CHECK_DOC8_XARGS := --ignore-path-errors "$(APP_ROOT)/docs/changes.rst;D000" + .PHONY: check-doc8-only check-doc8-only: mkdir-reports ## run PEP8 documentation style checks @echo "Running PEP8 doc style checks..." @-rm -fr "$(REPORTS_DIR)/check-doc8.txt" @bash -c '$(CONDA_CMD) \ doc8 --config "$(APP_ROOT)/setup.cfg" "$(APP_ROOT)/docs" \ + $(CHECK_DOC8_XARGS) \ 1> >(tee "$(REPORTS_DIR)/check-doc8.txt")' # FIXME: move parameters to setup.cfg when implemented (https://github.com/myint/docformatter/issues/10) diff --git a/magpie/owsrequest.py b/magpie/owsrequest.py index 34b1f2a39..72174414f 100644 --- a/magpie/owsrequest.py +++ b/magpie/owsrequest.py @@ -48,7 +48,6 @@ def ows_parser_factory(request): class OWSParser(object): - def __init__(self, request): self.request = request self.params = {} @@ -94,7 +93,6 @@ def _get_param_value(self, param): class OWSPostParser(OWSParser): - def __init__(self, request): super(OWSPostParser, self).__init__(request) self.document = xml_util.fromstring(self.request.body) diff --git a/magpie/services.py b/magpie/services.py index 190d565b7..d5260d446 100644 --- a/magpie/services.py +++ b/magpie/services.py @@ -1004,7 +1004,7 @@ def params_expected(cls): # noqa # pylint: disable=E0213,no-self-argument,W022 """ if cls.resource_param is None: return [] - elif isinstance(cls.resource_param, six.string_types): + if isinstance(cls.resource_param, six.string_types): impl_params = [cls.resource_param] elif isinstance(cls.resource_param, list): impl_params = cls.resource_param @@ -1527,7 +1527,7 @@ def service_supported(cls): # noqa # pylint: disable=E0213,no-self-argument,W0 Without the :class:`models.Workspace` scope in the path, ``identifier`` parameter fails to be resolved by `Geoserver`, as if it was unspecified. Attribute :attr:`ServiceGeoserverWPS.resource_scoped` controls the behaviour of splitting the defined :attr:`resource_param` into :class:`models.Workspace` and child components. - + .. note:: The :class:`models.Route` is allowed at the root of the service and for any nested :class:`models.Route` resource to support various endpoints such as the ``/web`` user interface, or the REST interface for the diff --git a/tests/test_adapter.py b/tests/test_adapter.py index cefb5c000..d32f8ba81 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -248,7 +248,6 @@ def mock_cookies(request): @runner.MAGPIE_TEST_ADAPTER @runner.MAGPIE_TEST_FUNCTIONAL class TestAdapterHooks(ti.SetupTwitcher, ti.UserTestCase, ti.BaseTestCase): - __test__ = True @classmethod From 491ccf4406a6267bad34b6f5143790233a01374b Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 30 Aug 2023 20:11:05 -0400 Subject: [PATCH 5/9] GeoServer API test fixes --- tests/test_services.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/test_services.py b/tests/test_services.py index b0d29e83d..a81510fac 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -1201,11 +1201,11 @@ def test_ServiceGeoserver_effective_permissions(self): # service calls svc_path = "/ows/proxy/{}".format(svc1_name) - def _msg(_path, _params): - # type: (Str, Dict[Str, Str]) -> Str + def _msg(_path, _params, _method): + # type: (Str, Dict[Str, Str], Str) -> Str _qs = "&".join("{}={}".format(k, v) for k, v in _params.items()) path_qs = "{}?{}".format(_path, _qs) if _qs else _path - return "Using combination [{}, {}]".format("GET", path_qs) + return "Using combination [{}, {}]".format(_method, path_qs) def _scope(workspace, layer): # type: (Str, Str) -> Str @@ -1215,9 +1215,16 @@ def _test(_path, _params, allow, method="GET"): # type: (Str, Dict[Str, Str], bool, Str) -> None req = self.mock_request(_path, method=method, params=_params) if allow: - utils.check_no_raise(lambda: self.ows.check_request(req), msg=_msg(_path, _params)) + utils.check_no_raise( + lambda: self.ows.check_request(req), + msg=_msg(_path, _params, method), + ) else: - utils.check_raises(lambda: self.ows.check_request(req), OWSAccessForbidden, msg=_msg(_path, _params)) + utils.check_raises( + lambda: self.ows.check_request(req), + OWSAccessForbidden, + msg=_msg(_path, _params, method), + ) # request for any OWS # /geoserver[/]/?request=GetCapabilities @@ -1252,9 +1259,10 @@ def _test(_path, _params, allow, method="GET"): # API endpoints only valid with exact paths r1_path = "{}/{}".format(svc_path, r1_name) - r2_path = "{}/{}".format(svc_path, r2_name) + r2_path = "{}/{}".format(r1_path, r2_name) r3_path = "{}/{}".format(svc_path, r3_name) - r4_path = "{}/{}".format(svc_path, r4_name) + r4_path = "{}/{}".format(r3_path, r4_name) + rnd_path = "{}/{}".format(svc_path, "random") _test(r1_path, {}, method="GET", allow=False) _test(r2_path, {}, method="GET", allow=True) _test(r3_path, {}, method="GET", allow=True) @@ -1263,8 +1271,10 @@ def _test(_path, _params, allow, method="GET"): _test(r2_path, {}, method="POST", allow=False) _test(r3_path, {}, method="POST", allow=True) _test(r4_path, {}, method="POST", allow=False) - _test("/random", {}, method="GET", allow=False) - _test("/random", {}, method="POST", allow=False) + _test(rnd_path, {}, method="GET", allow=False) + _test(rnd_path, {}, method="POST", allow=False) + _test(svc_path, {}, method="GET", allow=False) + _test(svc_path, {}, method="POST", allow=False) # Layer1, mismatching permission for WMS _test(svc_wms_path, {"request": Permission.GET_FEATURE.title, "layers": w1_l1}, allow=False) From e2e02be15aa1f16b6b8c434a72b646f198c9f327 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 30 Aug 2023 20:12:22 -0400 Subject: [PATCH 6/9] reduce readthedocs python 3.8 as latest available --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 960291ced..0a1f3e398 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -11,7 +11,7 @@ formats: #- pdf #- epub python: - version: "3.10" + version: "3.8" install: - requirements: requirements-sys.txt - requirements: requirements-doc.txt From df8aa1e5b4ba66bf7717b81d3635c9014b41401e Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 30 Aug 2023 20:59:00 -0400 Subject: [PATCH 7/9] fix linkcheck --- docs/conf.py | 3 ++- magpie/xml_util.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8c8bbea92..f08c74e92 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -215,7 +215,8 @@ def ignore_down_providers(): "https://pcmdi.llnl.gov/", # works, but very often causes false-positive 'broken' links ] + ignore_down_providers() linkcheck_anchors_ignore = [ - r".*issuecomment.*" # GitHub issue comment anchors not resolved + r".*issuecomment.*", # GitHub issue comment anchors not resolved + "defusedxmllxml", # not found because of GitHub dynamic links ] # Add any paths that contain templates here, relative to this directory. diff --git a/magpie/xml_util.py b/magpie/xml_util.py index 1415651b3..04d27e1a5 100644 --- a/magpie/xml_util.py +++ b/magpie/xml_util.py @@ -5,7 +5,7 @@ instead, because that package's extension with ``lxml`` is marked as deprecated. .. seealso:: - https://pypi.org/project/defusedxml/#defusedxml-lxml + https://github.com/tiran/defusedxml#defusedxmllxml To use the module, import is as if importing ``lxml.etree``: From 2a1290d02f640604bc774f470ff289a8c4c86433 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 30 Aug 2023 21:06:21 -0400 Subject: [PATCH 8/9] fix GeoServer tests --- tests/interfaces.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/interfaces.py b/tests/interfaces.py index 0a0b86363..8e0a5b3ab 100644 --- a/tests/interfaces.py +++ b/tests/interfaces.py @@ -6910,8 +6910,8 @@ def test_GetResourceTypes_ServiceGeoserver(self): Workspace.resource_type_name, ] struct_allowed = { - Service.resource_type_name: [Workspace.resource_type_name, Route.resource_type_name], - Route.resource_type_name: [Workspace.resource_type_name, Route.resource_type_name], + Service.resource_type_name: [Route.resource_type_name, Workspace.resource_type_name], + Route.resource_type_name: [Route.resource_type_name, Workspace.resource_type_name], Workspace.resource_type_name: [Layer.resource_type_name, Process.resource_type_name], Layer.resource_type_name: [], Process.resource_type_name: [], @@ -6934,7 +6934,7 @@ def test_GetResourceTypes_ServiceGeoserver(self): utils.check_val_is_in("resource_types_allowed", svc) utils.check_val_is_in("resource_structure_allowed", svc) utils.check_val_equal(svc["resource_child_allowed"], True) - utils.check_val_equal(svc["resource_types_allowed"], res_types_allowed) + utils.check_all_equal(svc["resource_types_allowed"], res_types_allowed, any_order=True) utils.check_val_equal(svc["resource_structure_allowed"], struct_allowed) path = "/resources/{}/types".format(svc_id) From 102af3378771bbbf53bd137027758ee369b3e638 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Wed, 30 Aug 2023 21:16:49 -0400 Subject: [PATCH 9/9] tmp fix workaround (relates to https://github.com/PyCQA/doc8/issues/145 and https://github.com/PyCQA/doc8/issues/147) --- Makefile | 5 +++-- setup.cfg | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1611d22d3..14c1d4ed4 100644 --- a/Makefile +++ b/Makefile @@ -564,8 +564,9 @@ check-security-code-only: mkdir-reports ## run security checks on source code .PHONY: check-docs-only check-docs-only: check-doc8-only check-docf-only check-links-only ## run every code documentation checks -# FIXME: temporary workaround (https://github.com/PyCQA/doc8/issues/145) -# configuration somehow not picked up directly from setup.cfg +# FIXME: temporary workaround (https://github.com/PyCQA/doc8/issues/145 and https://github.com/PyCQA/doc8/issues/147) +# configuration somehow not picked up directly from setup.cfg in python 3.11 +# setting 'ignore-path-errors' not working without the full path (relative 'docs/changes.rst' fails) CHECK_DOC8_XARGS := --ignore-path-errors "$(APP_ROOT)/docs/changes.rst;D000" .PHONY: check-doc8-only diff --git a/setup.cfg b/setup.cfg index c41e936f6..ef4c50955 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,6 +37,7 @@ universal = 1 [doc8] max-line-length = 120 ignore-path = docs/_build,docs/autoapi +ignore-path-errors = docs/changes.rst;D000, [flake8] ignore = E501,W291,W503,W504