diff -Nru mapproxy-1.12.0/CHANGES.txt mapproxy-1.13.0/CHANGES.txt --- mapproxy-1.12.0/CHANGES.txt 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/CHANGES.txt 2020-11-19 08:53:21.000000000 +0000 @@ -1,3 +1,20 @@ +1.13.0 2020-11-18 +~~~~~~~~~~~~~~~~~ + +Improvements: + +- Proj: Support for PROJ>=5 via pyproj. +- Services: New hide_exception_url option to hide source URLs. +- Tile sources: Support '@' in URL path (e.g. /0/0/0@2x.png) + +Fixes: + +- Various fixes for Python 3.8 compatibility. +- WMS: Always query WMS server in supported SRS. +- Fix warnings for tagged layer sources in layers. +- Demo: Fix capabilites "as HTML" when running behind a proxy + + 1.12.0 2019-08-30 ~~~~~~~~~~~~~~~~~ diff -Nru mapproxy-1.12.0/debian/changelog mapproxy-1.13.0/debian/changelog --- mapproxy-1.12.0/debian/changelog 2019-09-02 04:44:53.000000000 +0000 +++ mapproxy-1.13.0/debian/changelog 2020-12-19 10:00:00.000000000 +0000 @@ -1,3 +1,36 @@ +mapproxy (1.13.0-1~focal1) focal; urgency=medium + + * No change rebuild for Focal. + + -- Angelos Tzotsos Sat, 19 Dec 2020 12:00:00 +0200 + +mapproxy (1.13.0-1~focal0) focal; urgency=medium + + * No change rebuild for Focal. + + -- Angelos Tzotsos Sat, 12 Dec 2020 13:00:00 +0200 + +mapproxy (1.13.0-1) unstable; urgency=medium + + * New upstream release. + * Bump watch file version to 4. + * Drop patches applied/included upstream. + * Update lintian overrides. + + -- Bas Couwenberg Thu, 19 Nov 2020 17:15:50 +0100 + +mapproxy (1.12.0-2) unstable; urgency=medium + + * Update override for embedded-javascript-library. + * Bump Standards-Version to 4.5.0, no changes. + * Drop Name field from upstream metadata. + * Bump debhelper compat to 10. + * Add patches for compatibility with Python 3.8. + * Add patch to fix PIL version check. + * Add lintian override for manpage-without-executable. + + -- Bas Couwenberg Sun, 24 May 2020 15:00:06 +0200 + mapproxy (1.12.0-1) unstable; urgency=medium * Move from experimental to unstable. diff -Nru mapproxy-1.12.0/debian/compat mapproxy-1.13.0/debian/compat --- mapproxy-1.12.0/debian/compat 2018-07-20 17:53:56.000000000 +0000 +++ mapproxy-1.13.0/debian/compat 2020-05-24 12:59:44.000000000 +0000 @@ -1 +1 @@ -9 +10 diff -Nru mapproxy-1.12.0/debian/control mapproxy-1.13.0/debian/control --- mapproxy-1.12.0/debian/control 2019-09-01 11:07:44.000000000 +0000 +++ mapproxy-1.13.0/debian/control 2020-08-24 15:33:53.000000000 +0000 @@ -3,7 +3,7 @@ Uploaders: Bas Couwenberg Section: python Priority: optional -Build-Depends: debhelper (>= 9), +Build-Depends: debhelper (>= 10~), dh-python, libgdal-dev, python3-all, @@ -23,7 +23,7 @@ docbook-xsl, docbook-xml, xsltproc -Standards-Version: 4.4.0 +Standards-Version: 4.5.0 Vcs-Browser: https://salsa.debian.org/debian-gis-team/mapproxy Vcs-Git: https://salsa.debian.org/debian-gis-team/mapproxy.git Homepage: http://mapproxy.org/ diff -Nru mapproxy-1.12.0/debian/mapproxy-doc.lintian-overrides mapproxy-1.13.0/debian/mapproxy-doc.lintian-overrides --- mapproxy-1.12.0/debian/mapproxy-doc.lintian-overrides 2018-07-20 17:53:56.000000000 +0000 +++ mapproxy-1.13.0/debian/mapproxy-doc.lintian-overrides 2020-09-05 05:21:58.000000000 +0000 @@ -1,5 +1,5 @@ # libjs-twitter-bootstrap is not compatible -embedded-javascript-library usr/share/doc/mapproxy/html/_static/bootstrap-*/js/bootstrap.js please use libjs-twitter-bootstrap +embedded-javascript-library usr/share/doc/mapproxy/html/_static/bootstrap-*/js/bootstrap.js please use libjs-bootstrap font-in-non-font-package usr/share/doc/mapproxy/html/_static/boot*/fonts/* font-outside-font-dir usr/share/doc/mapproxy/html/_static/boots*/fonts/* diff -Nru mapproxy-1.12.0/debian/mapproxy.lintian-overrides mapproxy-1.13.0/debian/mapproxy.lintian-overrides --- mapproxy-1.12.0/debian/mapproxy.lintian-overrides 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.13.0/debian/mapproxy.lintian-overrides 2020-09-05 05:21:58.000000000 +0000 @@ -0,0 +1,3 @@ +# Manpage for subcommand +spare-manual-page usr/share/man/man1/mapproxy-util-autoconfig.1.gz + diff -Nru mapproxy-1.12.0/debian/python3-mapproxy.lintian-overrides mapproxy-1.13.0/debian/python3-mapproxy.lintian-overrides --- mapproxy-1.12.0/debian/python3-mapproxy.lintian-overrides 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.13.0/debian/python3-mapproxy.lintian-overrides 2020-11-19 16:15:50.000000000 +0000 @@ -0,0 +1,3 @@ +# Not a problem +breakout-link * + diff -Nru mapproxy-1.12.0/debian/source/lintian-overrides mapproxy-1.13.0/debian/source/lintian-overrides --- mapproxy-1.12.0/debian/source/lintian-overrides 2018-07-31 19:42:19.000000000 +0000 +++ mapproxy-1.13.0/debian/source/lintian-overrides 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -# Not worth the effort -testsuite-autopkgtest-missing - diff -Nru mapproxy-1.12.0/debian/upstream/metadata mapproxy-1.13.0/debian/upstream/metadata --- mapproxy-1.12.0/debian/upstream/metadata 2018-07-20 17:53:56.000000000 +0000 +++ mapproxy-1.13.0/debian/upstream/metadata 2020-05-24 12:59:44.000000000 +0000 @@ -1,6 +1,5 @@ --- Bug-Database: https://github.com/mapproxy/mapproxy/issues Bug-Submit: https://github.com/mapproxy/mapproxy/issues/new -Name: MapProxy Repository: https://github.com/mapproxy/mapproxy.git Repository-Browse: https://github.com/mapproxy/mapproxy diff -Nru mapproxy-1.12.0/debian/watch mapproxy-1.13.0/debian/watch --- mapproxy-1.12.0/debian/watch 2019-09-01 11:07:44.000000000 +0000 +++ mapproxy-1.13.0/debian/watch 2020-11-06 18:31:22.000000000 +0000 @@ -1,4 +1,4 @@ -version=3 +version=4 opts=\ dversionmangle=s/\+(debian|dfsg|ds|deb)\d*$//,\ uversionmangle=s/(\d)[_\.\-\+]?((RC|rc|pre|dev|beta|alpha)\d*)$/$1~$2/;s/RC/rc/,\ diff -Nru mapproxy-1.12.0/doc/configuration.rst mapproxy-1.13.0/doc/configuration.rst --- mapproxy-1.12.0/doc/configuration.rst 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/doc/configuration.rst 2020-11-19 08:53:21.000000000 +0000 @@ -908,10 +908,12 @@ .. versionadded:: 1.12.0 ``proj_data_dir`` - MapProxy uses Proj4 for all coordinate transformations. If you need custom projections - or need to tweak existing definitions (e.g. add towgs parameter set) you can point - MapProxy to your own set of proj4 init files. The path should contain an ``epsg`` file - with the EPSG definitions. + + MapProxy uses PROJ for all coordinate transformations. If you need custom projections + or need to tweak existing definitions. You can point MapProxy to your own set of PROJ data files. + + This path should contain an ``epsg`` file with the EPSG definitions for installations with PROJ version 4. + PROJ>=5 uses a different configuration format. Please refer to the PROJ documentation. The configured path can be absolute or relative to the mapproxy.yaml. @@ -943,6 +945,10 @@ If you need to override one of the default values, then you need to define both axis order options, even if one is empty. + .. versionchanged:: 1.13.0 + MapProxy can now determine the correct axis order for all coordinate systems when using pyproj>=2. The axis_order_ne/axis_order_en are ignored in this case. + + .. _http_ssl: ``http`` @@ -1019,6 +1025,15 @@ Sets the ``Access-control-allow-origin`` header to HTTP responses for `Cross-origin resource sharing `_. This header is required for WebGL or Canvas web clients. Defaults to `*`. Leave empty to disable the header. This option is only available in `globals`. +``hide_error_details`` +^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 1.13.0 + +When enabled, MapProxy will only report generic error messages to the client in case of any errors while fetching source services. +The full error message might contain confidential information like internal URLs. You will find the full error message in the logs, regardless of this option. The option is enabled by default, i.e. the details are hidden. + + ``tiles`` """""""""" diff -Nru mapproxy-1.12.0/doc/conf.py mapproxy-1.13.0/doc/conf.py --- mapproxy-1.12.0/doc/conf.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/doc/conf.py 2020-11-19 08:53:21.000000000 +0000 @@ -49,9 +49,9 @@ # built documents. # # The short X.Y version. -version = '1.12' +version = '1.13' # The full version, including alpha/beta/rc tags. -release = '1.12.0' +release = '1.13.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff -Nru mapproxy-1.12.0/doc/deployment.rst mapproxy-1.13.0/doc/deployment.rst --- mapproxy-1.12.0/doc/deployment.rst 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/doc/deployment.rst 2020-11-19 08:53:21.000000000 +0000 @@ -95,9 +95,9 @@ -``mod_wsgi`` has a lot of options for more fine tuning. ``WSGIPythonHome`` or ``WSGIPythonPath`` lets you configure your ``virtualenv`` and ``WSGIDaemonProcess``/``WSGIProcessGroup`` allows you to start multiple processes. See the `mod_wsgi configuration directives documentation `_. Using Mapnik also requires the ``WSGIApplicationGroup`` option. +``mod_wsgi`` has a lot of options for more fine tuning. ``WSGIPythonHome`` or ``WSGIPythonPath`` lets you configure your ``virtualenv`` and ``WSGIDaemonProcess``/``WSGIProcessGroup`` allows you to start multiple processes. See the `mod_wsgi configuration directives documentation `_. Using Mapnik also requires the ``WSGIApplicationGroup`` option. -.. note:: On Windows only the ``WSGIPythonPath`` option is supported. Linux/Unix supports ``WSGIPythonPath`` and ``WSGIPythonHome``. See also the `mod_wsgi documentation for virtualenv `_ for detailed information when using multiple virtualenvs. +.. note:: On Windows only the ``WSGIPythonPath`` option is supported. Linux/Unix supports ``WSGIPythonPath`` and ``WSGIPythonHome``. See also the `mod_wsgi documentation for virtualenv `_ for detailed information when using multiple virtualenvs. A more complete configuration might look like:: @@ -121,7 +121,7 @@ .. _mod_wsgi: http://www.modwsgi.org/ -.. _mod_wsgi installation: http://code.google.com/p/modwsgi/wiki/InstallationInstructions +.. _mod_wsgi installation: https://modwsgi.readthedocs.io/en/latest/installation.html Behind HTTP server or proxy --------------------------- diff -Nru mapproxy-1.12.0/doc/install.rst mapproxy-1.13.0/doc/install.rst --- mapproxy-1.12.0/doc/install.rst 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/doc/install.rst 2020-11-19 08:53:21.000000000 +0000 @@ -33,7 +33,7 @@ source mapproxy/bin/activate -This will change the ``PATH`` for you `current` session. +This will change the ``PATH`` for your `current` session. Install Dependencies @@ -48,7 +48,7 @@ On a Debian or Ubuntu system, you need to install the following packages:: - sudo apt-get install python-pil python-yaml libproj12 + sudo apt-get install python-pil python-yaml python-proj To get all optional packages:: @@ -59,15 +59,22 @@ Dependency details ^^^^^^^^^^^^^^^^^^ -libproj -~~~~~~~ -MapProxy uses the Proj4 C Library for all coordinate transformation tasks. It is included in most distributions as ``libproj`` or ``libprojXX`` where ``XX`` is a number. +pyproj or libproj +~~~~~~~~~~~~~~~~~ + +MapProxy uses the PROJ C library for all coordinate transformation tasks. MapProxy can directly use the C library or via the pyproj Python package. +The internal API of PROJ was updated with PROJ >=5. The old PROJ 4 API is now deprecated and will be removed from future PROJ releases. MapProxy only supports the new API via pyproj and it is therefore recommended to use a recent pyproj version. + + +.. versionchanged:: 1.13 + Support for new PROJ API via pyproj. + .. _dependencies_pil: Pillow ~~~~~~ -Pillow, the successor of the Python Image Library (PIL), is used for the image processing and it is included in most distributions as ``python-pil`` or ``python-imaging``. Please make sure that you have Pillow installed as MapProxy is no longer compatible with the original PIL. The version of ``python-imaging`` should be >=2. +Pillow, the successor of the Python Image Library (PIL), is used for the image processing and it is included in most distributions as ``python-pil`` or ``python-imaging``. Please make sure that you have Pillow installed as MapProxy is no longer compatible with the original PIL. The version of ``python-imaging`` should be >=3.1. You can install a new version of Pillow from source with:: @@ -101,7 +108,7 @@ Install MapProxy ---------------- -Your virtual environment should contains `pip`_, a tool to install Python packages. +Your virtual environment should contain `pip`_, a tool to install Python packages. To install you need to call:: @@ -179,6 +186,6 @@ Changes ^^^^^^^ -New releases of MapProxy are backwards compatible with older configuration files. MapProxy will issue warnings on startup if a behavior will change in the next releases. You are advised to upgrade in single release steps (e.g. 1.9.0 to 1.10.0 to 1.11.0) and to check the output of ``mapproxy-util serve-develop`` for any warnings. You should also refer to the Changes Log of each release to see if there is anything to pay attention for. +New releases of MapProxy are backwards compatible with older configuration files. MapProxy will issue warnings on start-up if a behavior will change in the next releases. You are advised to upgrade in single release steps (e.g. 1.9.0 to 1.10.0 to 1.11.0) and to check the output of ``mapproxy-util serve-develop`` for any warnings. You should also refer to the Changes Log of each release to see if there is anything to pay attention for. -If you upgrade from 0.8, please read the `old mirgation documentation `_. +If you upgrade from 0.8, please read the `old migration documentation `_. diff -Nru mapproxy-1.12.0/doc/install_windows.rst mapproxy-1.13.0/doc/install_windows.rst --- mapproxy-1.12.0/doc/install_windows.rst 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/doc/install_windows.rst 2020-11-19 08:53:21.000000000 +0000 @@ -52,12 +52,13 @@ PyProj ~~~~~~ -Since libproj4 is generally not available on a Windows system, you will also need to install the Python package ``pyproj``. -You need to manually download the ``pyproj`` package for your system. See below for *Platform dependent packages*. +Since PROJ is generally not available on a Windows system, you will also need to install the Python package ``pyproj``. :: - pip install path\to\pyproj-xxx.whl + pip install pyproj + +See *Platform dependent packages* below if this installation fails as Windows packages might not be available for pyproj. Shapely and GEOS *(optional)* diff -Nru mapproxy-1.12.0/.github/pull_request_template.md mapproxy-1.13.0/.github/pull_request_template.md --- mapproxy-1.12.0/.github/pull_request_template.md 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.13.0/.github/pull_request_template.md 2020-11-19 08:53:21.000000000 +0000 @@ -0,0 +1,20 @@ + diff -Nru mapproxy-1.12.0/mapproxy/cache/couchdb.py mapproxy-1.13.0/mapproxy/cache/couchdb.py --- mapproxy-1.12.0/mapproxy/cache/couchdb.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/cache/couchdb.py 2020-11-19 08:53:21.000000000 +0000 @@ -71,7 +71,7 @@ self.req_session.put(self.couch_url) self.db_initialised = True except requests.exceptions.RequestException as ex: - log.warn('unable to initialize CouchDB: %s', ex) + log.warning('unable to initialize CouchDB: %s', ex) def tile_url(self, coord): return self.document_url(coord) + '/tile' @@ -109,7 +109,7 @@ except (requests.exceptions.RequestException, socket.error) as ex: # is_cached should not fail (would abort seeding for example), # so we catch these errors here and just return False - log.warn('error while requesting %s: %s', url, ex) + log.warning('error while requesting %s: %s', url, ex) return False if resp.status_code == 404: return False diff -Nru mapproxy-1.12.0/mapproxy/cache/geopackage.py mapproxy-1.13.0/mapproxy/cache/geopackage.py --- mapproxy-1.12.0/mapproxy/cache/geopackage.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/cache/geopackage.py 2020-11-19 08:53:21.000000000 +0000 @@ -395,7 +395,7 @@ cursor.executemany(stmt, records) self.db.commit() except sqlite3.OperationalError as ex: - log.warn('unable to store tile: %s', ex) + log.warning('unable to store tile: %s', ex) return False return True diff -Nru mapproxy-1.12.0/mapproxy/cache/mbtiles.py mapproxy-1.13.0/mapproxy/cache/mbtiles.py --- mapproxy-1.12.0/mapproxy/cache/mbtiles.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/cache/mbtiles.py 2020-11-19 08:53:21.000000000 +0000 @@ -178,7 +178,7 @@ cursor.executemany(stmt, records) self.db.commit() except sqlite3.OperationalError as ex: - log.warn('unable to store tile: %s', ex) + log.warning('unable to store tile: %s', ex) return False return True diff -Nru mapproxy-1.12.0/mapproxy/cache/riak.py mapproxy-1.13.0/mapproxy/cache/riak.py --- mapproxy-1.12.0/mapproxy/cache/riak.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/cache/riak.py 2020-11-19 08:53:21.000000000 +0000 @@ -72,7 +72,7 @@ try: obj = self.bucket.get(key, r=1, timeout=self.request_timeout) except Exception as e: - log.warn('error while requesting %s: %s', key, e) + log.warning('error while requesting %s: %s', key, e) if not obj: obj = self.bucket.new(key=key, data=None, content_type='application/octet-stream') @@ -107,7 +107,7 @@ try: res.store(w=1, dw=1, pw=1, return_body=False, timeout=self.request_timeout) except riak.RiakError as ex: - log.warn('unable to store tile: %s', ex) + log.warning('unable to store tile: %s', ex) return False return True @@ -158,7 +158,7 @@ try: res.delete(w=1, r=1, dw=1, pw=1, timeout=self.request_timeout) except riak.RiakError as ex: - log.warn('unable to remove tile: %s', ex) + log.warning('unable to remove tile: %s', ex) return False return True diff -Nru mapproxy-1.12.0/mapproxy/cache/s3.py mapproxy-1.13.0/mapproxy/cache/s3.py --- mapproxy-1.12.0/mapproxy/cache/s3.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/cache/s3.py 2020-11-19 08:53:21.000000000 +0000 @@ -55,6 +55,7 @@ super(S3Cache, self).__init__() self.lock_cache_id = hashlib.md5(base_path.encode('utf-8') + bucket_name.encode('utf-8')).hexdigest() self.bucket_name = bucket_name + self.profile_name = profile_name self.region_name = region_name self.endpoint_url = endpoint_url self.access_control_list = access_control_list @@ -86,7 +87,7 @@ raise ImportError("S3 Cache requires 'boto3' package.") try: - return s3_session().client("s3", region_name=self.region_name, endpoint_url=self.endpoint_url) + return s3_session(self.profile_name).client("s3", region_name=self.region_name, endpoint_url=self.endpoint_url) except Exception as e: raise S3ConnectionError('Error during connection %s' % e) diff -Nru mapproxy-1.12.0/mapproxy/client/cgi.py mapproxy-1.13.0/mapproxy/client/cgi.py --- mapproxy-1.12.0/mapproxy/client/cgi.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/client/cgi.py 2020-11-19 08:53:21.000000000 +0000 @@ -118,7 +118,7 @@ else: headers, content = split_cgi_response(stdout) - status_match = re.match('(\d\d\d) ', headers.get('Status', '')) + status_match = re.match(r'(\d\d\d) ', headers.get('Status', '')) if status_match: status_code = status_match.group(1) else: diff -Nru mapproxy-1.12.0/mapproxy/client/http.py mapproxy-1.13.0/mapproxy/client/http.py --- mapproxy-1.12.0/mapproxy/client/http.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/client/http.py 2020-11-19 08:53:21.000000000 +0000 @@ -45,9 +45,10 @@ supports_ssl_default_context = True class HTTPClientError(Exception): - def __init__(self, arg, response_code=None): + def __init__(self, arg, response_code=None, full_msg=None): Exception.__init__(self, arg) self.response_code = response_code + self.full_msg = full_msg def build_https_handler(ssl_ca_certs, insecure): @@ -160,7 +161,7 @@ class HTTPClient(object): def __init__(self, url=None, username=None, password=None, insecure=False, - ssl_ca_certs=None, timeout=None, headers=None): + ssl_ca_certs=None, timeout=None, headers=None, hide_error_details=False): self._timeout = timeout if url and url.startswith('https'): if insecure: @@ -171,6 +172,7 @@ self.opener = create_url_opener(ssl_ca_certs, url, username, password, insecure=insecure) self.header_list = headers.items() if headers else [] + self.hide_error_details = hide_error_details def open(self, url, data=None): code = None @@ -178,8 +180,8 @@ try: req = urllib2.Request(url, data=data) except ValueError as e: - reraise_exception(HTTPClientError('URL not correct "%s": %s' - % (url, e.args[0])), sys.exc_info()) + err = self.handle_url_exception(url, 'URL not correct', e.args[0]) + reraise_exception(err, sys.exc_info()) for key, value in self.header_list: req.add_header(key, value) try: @@ -190,25 +192,24 @@ result = self.opener.open(req) except HTTPError as e: code = e.code - reraise_exception(HTTPClientError('HTTP Error "%s": %d' - % (url, e.code), response_code=code), sys.exc_info()) + err = self.handle_url_exception(url, 'HTTP Error', str(code), response_code=code) + reraise_exception(err, sys.exc_info()) except URLError as e: if isinstance(e.reason, ssl.SSLError): - e = HTTPClientError('Could not verify connection to URL "%s": %s' - % (url, e.reason.args[1])) - reraise_exception(e, sys.exc_info()) + err = self.handle_url_exception(url, 'Could not verify connection to URL', e.reason.args[1]) + reraise_exception(err, sys.exc_info()) try: reason = e.reason.args[1] except (AttributeError, IndexError): reason = e.reason - reraise_exception(HTTPClientError('No response from URL "%s": %s' - % (url, reason)), sys.exc_info()) + err = self.handle_url_exception(url, 'No response from URL', reason) + reraise_exception(err, sys.exc_info()) except ValueError as e: - reraise_exception(HTTPClientError('URL not correct "%s": %s' - % (url, e.args[0])), sys.exc_info()) + err = self.handle_url_exception(url, 'URL not correct', e.args[0]) + reraise_exception(err, sys.exc_info()) except Exception as e: - reraise_exception(HTTPClientError('Internal HTTP error "%s": %r' - % (url, e)), sys.exc_info()) + err = self.handle_url_exception(url, 'Internal HTTP error', repr(e)) + reraise_exception(err, sys.exc_info()) else: code = getattr(result, 'code', 200) if code == 204: @@ -224,8 +225,24 @@ raise HTTPClientError('response is not an image: (%s)' % (resp.read())) return ImageSource(resp) + def handle_url_exception(self, url, message, reason, response_code=None): + full_msg = '%s "%s": %s' % (message, url, reason) + if self.hide_error_details: + return HTTPClientError( + '{} (see logs for URL and reason).'.format(message), + response_code=response_code, + full_msg=full_msg, + ) + else: + return HTTPClientError( + full_msg, + response_code=response_code, + ) + def auth_data_from_url(url): """ + >>> auth_data_from_url('invalid_url') + ('invalid_url', (None, None)) >>> auth_data_from_url('http://localhost/bar') ('http://localhost/bar', (None, None)) >>> auth_data_from_url('http://bar@localhost/bar') @@ -238,16 +255,35 @@ ('http://localhost/bar', ('bar foo; foo@bar', 'b:az@')) >>> auth_data_from_url('https://bar:foo#;%$@localhost/bar') ('https://localhost/bar', ('bar', 'foo#;%$')) - """ + >>> auth_data_from_url('http://localhost/bar@2x') + ('http://localhost/bar@2x', (None, None)) + >>> auth_data_from_url('http://bar@localhost/bar@2x') + ('http://localhost/bar@2x', ('bar', None)) + >>> auth_data_from_url('http://bar:baz@localhost/bar@2x') + ('http://localhost/bar@2x', ('bar', 'baz')) + >>> auth_data_from_url('https://bar@localhost/bar/0/0/0@2x.png') + ('https://localhost/bar/0/0/0@2x.png', ('bar', None)) + >>> auth_data_from_url('http://bar:baz@localhost/bar@2x.png') + ('http://localhost/bar@2x.png', ('bar', 'baz')) + """ + if not url or '://' not in url: + # be silent for invalid URLs + return url, (None, None) + + schema, url = url.split('://', 1) + if '/' in url: + host, request = url.split('/', 1) + else: + host, request = url, '' + username = password = None - if '@' in url: - head, url = url.rsplit('@', 1) - schema, auth_data = head.split('//', 1) - url = schema + '//' + url + if '@' in host: + auth_data, host = host.rsplit('@', 1) if ':' in auth_data: username, password = auth_data.split(':', 1) else: username = auth_data + url = schema + "://" + host + "/" + request return url, (username, password) diff -Nru mapproxy-1.12.0/mapproxy/client/wms.py mapproxy-1.13.0/mapproxy/client/wms.py --- mapproxy-1.12.0/mapproxy/client/wms.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/client/wms.py 2020-11-19 08:53:21.000000000 +0000 @@ -16,6 +16,8 @@ """ WMS clients for maps and information. """ +import sys + from mapproxy.compat import text_type from mapproxy.request.base import split_mime_type from mapproxy.layer import InfoQuery @@ -72,10 +74,19 @@ log_size = 8000 # larger xml exception else: log_size = 100 # image? - data = resp.read(log_size) - if len(data) == log_size: - data += '... truncated' - log.warn("no image returned from source WMS: %s, response was: %s" % (url, data)) + data = resp.read(log_size+1) + + truncated = '' + if len(data) == log_size+1: + data = data[:-1] + truncated = ' [output truncated]' + + if sys.version_info >= (3, 5, 0): + data = data.decode('utf-8', 'backslashreplace') + else: + data = data.decode('ascii', 'ignore') + + log.warning("no image returned from source WMS: {}, response was: '{}'{}".format(url, data, truncated)) raise SourceError('no image returned from source WMS: %s' % (url, )) def _query_url(self, query, format): diff -Nru mapproxy-1.12.0/mapproxy/compat/image.py mapproxy-1.13.0/mapproxy/compat/image.py --- mapproxy-1.12.0/mapproxy/compat/image.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/compat/image.py 2020-11-19 08:53:21.000000000 +0000 @@ -25,6 +25,7 @@ # prevent pyflakes warnings Image, ImageColor, ImageDraw, ImageFont, ImagePalette, ImageChops, ImageMath ImageFileDirectory_v2, TiffTags + PIL_VERSION = getattr(PIL, '__version__') or getattr(PIL, 'PILLOW_VERSION') except ImportError: # allow MapProxy to start without PIL (for tilecache only). # issue warning and raise ImportError on first use of @@ -42,17 +43,17 @@ Image.Image = NoPIL ImageColor = NoPIL() ImageColor.getrgb = lambda x: x + PIL_VERSION = None def has_alpha_composite_support(): return hasattr(Image, 'alpha_composite') def transform_uses_center(): - # transformation behavior changed with Pillow 3.4 + # transformation behavior changed with Pillow 3.4 to use pixel centers # https://github.com/python-pillow/Pillow/commit/5232361718bae0f0ccda76bfd5b390ebf9179b18 - if hasattr(PIL, 'PILLOW_VERSION'): - if not PIL.PILLOW_VERSION.startswith(('1.', '2.', '3.0', '3.1', '3.2', '3.3')): - return True - return False + if not PIL_VERSION or PIL_VERSION.startswith(('1.', '2.', '3.0', '3.1', '3.2', '3.3')): + return False + return True def quantize_pil(img, colors=256, alpha=False, defaults=None): if hasattr(Image, 'FASTOCTREE'): diff -Nru mapproxy-1.12.0/mapproxy/compat/modules.py mapproxy-1.13.0/mapproxy/compat/modules.py --- mapproxy-1.12.0/mapproxy/compat/modules.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/compat/modules.py 2020-11-19 08:53:21.000000000 +0000 @@ -5,5 +5,9 @@ if PY2: import urlparse; urlparse + from cgi import parse_qsl, escape + from urllib import quote else: - from urllib import parse as urlparse \ No newline at end of file + from html import escape + from urllib import parse as urlparse + from urllib.parse import parse_qsl, quote diff -Nru mapproxy-1.12.0/mapproxy/config/defaults.py mapproxy-1.13.0/mapproxy/config/defaults.py --- mapproxy-1.12.0/mapproxy/config/defaults.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/config/defaults.py 2020-11-19 08:53:21.000000000 +0000 @@ -93,4 +93,5 @@ concurrent_requests = 0, method = 'AUTO', access_control_allow_origin = '*', + hide_error_details = True, ) diff -Nru mapproxy-1.12.0/mapproxy/config/loader.py mapproxy-1.13.0/mapproxy/config/loader.py --- mapproxy-1.12.0/mapproxy/config/loader.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/config/loader.py 2020-11-19 08:53:21.000000000 +0000 @@ -293,7 +293,7 @@ global_key='image.max_shrink_factor') if conf.get('origin') is None: - log.warn('grid %s does not have an origin. default origin will change from sw (south/west) to nw (north-west) with MapProxy 2.0', + log.warning('grid %s does not have an origin. default origin will change from sw (south/west) to nw (north-west) with MapProxy 2.0', conf['name'], ) @@ -591,10 +591,11 @@ timeout = self.context.globals.get_value('http.client_timeout', self.conf) headers = self.context.globals.get_value('http.headers', self.conf) + hide_error_details = self.context.globals.get_value('http.hide_error_details', self.conf) http_client = HTTPClient(url, username, password, insecure=insecure, ssl_ca_certs=ssl_ca_certs, timeout=timeout, - headers=headers) + headers=headers, hide_error_details=hide_error_details) return http_client, url @memoize @@ -974,7 +975,7 @@ grid_name = self.conf.get('grid') if grid_name is None: - log.warn("tile source for %s does not have a grid configured and defaults to GLOBAL_MERCATOR. default will change with MapProxy 2.0", url) + log.warning("tile source for %s does not have a grid configured and defaults to GLOBAL_MERCATOR. default will change with MapProxy 2.0", url) grid_name = "GLOBAL_MERCATOR" grid = self.context.grids[grid_name].tile_grid() @@ -1022,7 +1023,7 @@ cache_dir = self.conf.get('cache', {}).get('directory') if cache_dir: if self.conf.get('cache_dir'): - log.warn('found cache.directory and cache_dir option for %s, ignoring cache_dir', + log.warning('found cache.directory and cache_dir option for %s, ignoring cache_dir', self.conf['name']) return self.context.globals.abspath(cache_dir) @@ -1060,7 +1061,7 @@ global_key='cache.link_single_color_images') if link_single_color_images and sys.platform == 'win32': - log.warn('link_single_color_images not supported on windows') + log.warning('link_single_color_images not supported on windows') link_single_color_images = False return FileCache( @@ -1451,8 +1452,8 @@ from mapproxy.layer import map_extent_from_grid, merge_layer_extents base_image_opts = self.image_opts() - if self.conf.get('format') == 'mixed' and not self.conf.get('request_format') == 'image/png': - raise ConfigurationError('request_format must be set to image/png if mixed mode is enabled') + if self.conf.get('format') == 'mixed' and not self.conf.get('request_format') in [ 'image/png', 'image/vnd.jpeg-png' ]: + raise ConfigurationError('request_format must be set to image/png or image/vnd.jpeg-png if mixed mode is enabled') request_format = self.conf.get('request_format') or self.conf.get('format') if '/' in request_format: request_format_ext = request_format.split('/', 1)[1] @@ -1581,7 +1582,7 @@ def grid_confs(self): grid_names = self.conf.get('grids') if grid_names is None: - log.warn('cache %s does not have any grids. default will change from [GLOBAL_MERCATOR] to [GLOBAL_WEBMERCATOR] with MapProxy 2.0', + log.warning('cache %s does not have any grids. default will change from [GLOBAL_MERCATOR] to [GLOBAL_WEBMERCATOR] with MapProxy 2.0', self.conf['name']) grid_names = ['GLOBAL_MERCATOR'] return [(g, self.context.grids[g]) for g in grid_names] @@ -1936,7 +1937,7 @@ fi_template = conf.get('restful_featureinfo_template') if template and '{{' in template: # TODO remove warning in 1.6 - log.warn("double braces in WMTS restful_template are deprecated {{x}} -> {x}") + log.warning("double braces in WMTS restful_template are deprecated {{x}} -> {x}") services.append( WMTSRestServer( layers, md, template=template, @@ -1974,7 +1975,7 @@ for format in image_formats_names: opts = self.context.globals.image_options.image_opts({}, format) if opts.format in image_formats: - log.warn('duplicate mime-type for WMS image_formats: "%s" already configured, will use last format', + log.warning('duplicate mime-type for WMS image_formats: "%s" already configured, will use last format', opts.format) image_formats[opts.format] = opts info_types = conf.get('featureinfo_types') @@ -2071,13 +2072,13 @@ errors, informal_only = validate_options(conf_dict) for error in errors: - log.warn(error) + log.warning(error) if not informal_only or (errors and not ignore_warnings): raise ConfigurationError('invalid configuration') errors = validate_references(conf_dict) for error in errors: - log.warn(error) + log.warning(error) return ProxyConfiguration(conf_dict, conf_base_dir=conf_base_dir, seed=seed, renderd=renderd) @@ -2149,5 +2150,3 @@ return r, g, b, a return r, g, b - - diff -Nru mapproxy-1.12.0/mapproxy/config/spec.py mapproxy-1.13.0/mapproxy/config/spec.py --- mapproxy-1.12.0/mapproxy/config/spec.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/config/spec.py 2020-11-19 08:53:21.000000000 +0000 @@ -71,6 +71,7 @@ 'client_timeout': number(), 'ssl_no_cert_checks': bool(), 'ssl_ca_certs': str(), + 'hide_error_details': bool(), 'headers': { anything(): str() }, diff -Nru mapproxy-1.12.0/mapproxy/config/validator.py mapproxy-1.13.0/mapproxy/config/validator.py --- mapproxy-1.12.0/mapproxy/config/validator.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/config/validator.py 2020-11-19 08:53:21.000000000 +0000 @@ -80,8 +80,9 @@ if source in self.caches_conf: self._validate_cache(source, self.caches_conf[source]) continue + + source, layers = self._split_tagged_source(source) if source in self.sources_conf: - source, layers = self._split_tagged_source(source) self._validate_source(source, self.sources_conf[source], layers) continue @@ -104,7 +105,6 @@ ) ) - def _split_tagged_source(self, source_name): layers = None if ':' in str(source_name): diff -Nru mapproxy-1.12.0/mapproxy/exception.py mapproxy-1.13.0/mapproxy/exception.py --- mapproxy-1.12.0/mapproxy/exception.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/exception.py 2020-11-19 08:53:21.000000000 +0000 @@ -16,8 +16,8 @@ """ Service exception handling (WMS exceptions, XML, in_image, etc.). """ -import cgi from mapproxy.response import Response +from mapproxy.compat.modules import escape class RequestError(Exception): """ @@ -44,11 +44,13 @@ """ if self.request is not None: handler = self.request.exception_handler - return handler.render(self) + resp = handler.render(self) elif self.status is not None: - return Response(self.msg, status=self.status) + resp = Response(self.msg, status=self.status) else: - return Response('internal error: %s' % self.msg, status=500) + resp = Response('internal error: %s' % self.msg, status=500) + resp.cache_headers(no_cache=True) + return resp def __str__(self): return 'RequestError("%s", code=%r, request=%r)' % (self.msg, self.code, @@ -116,7 +118,7 @@ """ status_code = self.status_codes.get(request_error.code, self.status_code) # escape &<> in error message (e.g. URL params) - msg = cgi.escape(request_error.msg) + msg = escape(request_error.msg) result = self.template.substitute(exception=msg, code=request_error.code) return Response(result, mimetype=self.mimetype, content_type=self.content_type, diff -Nru mapproxy-1.12.0/mapproxy/image/message.py mapproxy-1.13.0/mapproxy/image/message.py --- mapproxy-1.12.0/mapproxy/image/message.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/image/message.py 2020-11-19 08:53:21.000000000 +0000 @@ -99,11 +99,11 @@ self.font_size) except ImportError: _pil_ttf_support = False - log_system.warn("Couldn't load TrueType fonts, " + log_system.warning("Couldn't load TrueType fonts, " "PIL needs to be build with freetype support.") except IOError: _pil_ttf_support = False - log_system.warn("Couldn't load find TrueType font ", self.font_name) + log_system.warning("Couldn't load find TrueType font ", self.font_name) if self._font is None: self._font = ImageFont.load_default() return self._font diff -Nru mapproxy-1.12.0/mapproxy/image/tile.py mapproxy-1.13.0/mapproxy/image/tile.py --- mapproxy-1.12.0/mapproxy/image/tile.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/image/tile.py 2020-11-19 08:53:21.000000000 +0000 @@ -65,7 +65,7 @@ source.close_buffers() except IOError as e: if e.errno is None: # PIL error - log.warn('unable to load tile %s, removing it (reason was: %s)' + log.warning('unable to load tile %s, removing it (reason was: %s)' % (source, str(e))) if getattr(source, 'filename'): if os.path.exists(source.filename): diff -Nru mapproxy-1.12.0/mapproxy/layer.py mapproxy-1.13.0/mapproxy/layer.py --- mapproxy-1.12.0/mapproxy/layer.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/layer.py 2020-11-19 08:53:21.000000000 +0000 @@ -429,7 +429,7 @@ num_tiles = tile_grid[0] * tile_grid[1] if self.max_tile_limit and num_tiles >= self.max_tile_limit: - raise MapBBOXError("too many tiles") + raise MapBBOXError("too many tiles, max_tile_limit: %s, num_tiles: %s" % (self.max_tile_limit, num_tiles)) if query.tiled_only: if num_tiles > 1: diff -Nru mapproxy-1.12.0/mapproxy/proj.py mapproxy-1.13.0/mapproxy/proj.py --- mapproxy-1.12.0/mapproxy/proj.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/proj.py 2020-11-19 08:53:21.000000000 +0000 @@ -14,7 +14,7 @@ # limitations under the License. """ -ctypes based replacement of pyroj (with pyproj fallback). +ctypes based replacement of pyroj (with pyproj fallback) of the old PROJ 4 API. This module implements the `Proj`, `transform` and `set_datapath` class/functions. This module is a drop-in replacement for pyproj. It does implement just enough to work for @@ -24,6 +24,8 @@ to pyroj. You can force the usage of either backend by setting the environment variables MAPPROXY_USE_LIBPROJ or MAPPROXY_USE_PYPROJ to any value. +The new PROJ >=5 API is only supported via pyproj. See USE_PROJ4_API. + """ from __future__ import print_function @@ -57,6 +59,10 @@ if libproj is None: return + if hasattr(libproj, 'proj_create'): + log_system.warning('Found libproj >=5. Using this library without pyproj is ' + 'deprecated and not fully supported. Please install pyproj >= 2.') + libproj.pj_init_plus.argtypes = [c_char_p] libproj.pj_init_plus.restype = c_void_p @@ -127,14 +133,24 @@ class ProjInitError(ProjError): pass -def try_pyproj_import(): +def try_pyproj4_import(): try: from pyproj import Proj, transform, set_datapath except ImportError: return False - log_system.info('using pyproj for coordinate transformation') + log_system.info('using pyproj with old Proj4 API for coordinate transformation') return Proj, transform, set_datapath +def try_pyproj_import(): + try: + from pyproj import CRS + from pyproj.transformer import Transformer + from pyproj.datadir import set_data_dir + except ImportError: + return False + log_system.info('using pyproj for coordinate transformation') + return CRS, Transformer, set_data_dir + def try_libproj_import(): libproj = init_libproj() @@ -244,22 +260,33 @@ proj_imports = [try_libproj_import] if 'MAPPROXY_USE_PYPROJ' in os.environ: - proj_imports = [try_pyproj_import] + proj_imports = [try_pyproj_import, try_pyproj4_import] if not proj_imports: if sys.platform == 'win32': # prefer pyproj on windows - proj_imports = [try_pyproj_import, try_libproj_import] + proj_imports = [try_pyproj_import, try_pyproj4_import, try_libproj_import] else: - proj_imports = [try_libproj_import, try_pyproj_import] + proj_imports = [try_pyproj_import, try_libproj_import, try_pyproj4_import] +# try different imports in previously defined order for try_import in proj_imports: - res = try_import() - if res: - Proj, transform, set_datapath = res - break + if try_import == try_pyproj_import: + res = try_import() + if res: + CRS, Transformer, set_datapath = res + Proj, transform = None, None + USE_PROJ4_API = False + break + else: + res = try_import() + if res: + Proj, transform, set_datapath = res + CRS, Transformer = None, None + USE_PROJ4_API = True + break else: - raise ImportError('could not find libproj or pyproj') + raise ImportError('could not find pyproj (Python library) or libproj (C library, deprecated)') if __name__ == '__main__': diff -Nru mapproxy-1.12.0/mapproxy/request/base.py mapproxy-1.13.0/mapproxy/request/base.py --- mapproxy-1.12.0/mapproxy/request/base.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/request/base.py 2020-11-19 08:53:21.000000000 +0000 @@ -16,15 +16,9 @@ """ Service requests (parsing, handling, etc). """ -import cgi - +from mapproxy.compat import PY2, iteritems, text_type +from mapproxy.compat.modules import parse_qsl, urlparse, quote from mapproxy.util.py import cached_property -from mapproxy.compat import iteritems, PY2, text_type - -if PY2: - from urllib import quote -else: - from urllib.parse import quote class NoCaseMultiDict(dict): """ @@ -178,7 +172,7 @@ Parse query string `qs` and return a `NoCaseMultiDict`. """ tmp = [] - for key, value in cgi.parse_qsl(qs, include_empty): + for key, value in parse_qsl(qs, include_empty): if PY2: if decode_keys: key = key.decode(charset, errors) @@ -265,12 +259,27 @@ def host_url(self): return '%s://%s/' % (self.url_scheme, self.host) + @cached_property + def server_url(self): + return 'http://%s:%s/' % ( + self.environ['SERVER_NAME'], + self.environ['SERVER_PORT'] + ) + @property def script_url(self): "Full script URL without trailing /" return (self.host_url.rstrip('/') + quote(self.environ.get('SCRIPT_NAME', '/').rstrip('/')) ) + + @property + def server_script_url(self): + "Internal script URL" + return self.script_url.replace( + self.host_url.rstrip('/'), + self.server_url.rstrip('/') + ) @property def base_url(self): diff -Nru mapproxy-1.12.0/mapproxy/request/wms/__init__.py mapproxy-1.13.0/mapproxy/request/wms/__init__.py --- mapproxy-1.12.0/mapproxy/request/wms/__init__.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/request/wms/__init__.py 2020-11-19 08:53:21.000000000 +0000 @@ -331,7 +331,7 @@ if SRS(srs).is_axis_order_ne: return bbox[1], bbox[0], bbox[3], bbox[2] except RuntimeError: - log.warn('unknown SRS %s' % srs) + log.warning('unknown SRS %s' % srs) return bbox def _switch_bbox(self): diff -Nru mapproxy-1.12.0/mapproxy/request/wmts.py mapproxy-1.13.0/mapproxy/request/wmts.py --- mapproxy-1.12.0/mapproxy/request/wmts.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/request/wmts.py 2020-11-19 08:53:21.000000000 +0000 @@ -318,7 +318,7 @@ if self._regexp: return self._regexp converted_re = self.var_re.sub(self.substitute_var, re.escape(self.template)) - wmts_re = re.compile('/wmts' + converted_re) + wmts_re = re.compile(r'/wmts' + converted_re) if not self.found.issuperset(self.required): raise InvalidWMTSTemplate('missing required variables in WMTS restful template: %s' % self.required.difference(self.found)) diff -Nru mapproxy-1.12.0/mapproxy/script/conf/sources.py mapproxy-1.13.0/mapproxy/script/conf/sources.py --- mapproxy-1.12.0/mapproxy/script/conf/sources.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/script/conf/sources.py 2020-11-19 08:53:21.000000000 +0000 @@ -34,7 +34,7 @@ SRS(srs) _checked_srs[srs] = True except Exception as ex: - logging.getLogger(__name__).warn('unable to initialize srs for %s: %s', srs, ex) + logging.getLogger(__name__).warning('unable to initialize srs for %s: %s', srs, ex) _checked_srs[srs] = False return _checked_srs[srs] diff -Nru mapproxy-1.12.0/mapproxy/script/defrag.py mapproxy-1.13.0/mapproxy/script/defrag.py --- mapproxy-1.12.0/mapproxy/script/defrag.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/script/defrag.py 2020-11-19 08:53:21.000000000 +0000 @@ -108,7 +108,7 @@ >>> bundle_offset("path/to/R0380C1380.bundle") (4992, 896) """ - match = re.search('R([A-F0-9]{4,})C([A-F0-9]{4,}).bundle$', fname, re.IGNORECASE) + match = re.search(r'R([A-F0-9]{4,})C([A-F0-9]{4,}).bundle$', fname, re.IGNORECASE) if match: r = int(match.group(1), 16) c = int(match.group(2), 16) diff -Nru mapproxy-1.12.0/mapproxy/script/export.py mapproxy-1.13.0/mapproxy/script/export.py --- mapproxy-1.12.0/mapproxy/script/export.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/script/export.py 2020-11-19 08:53:21.000000000 +0000 @@ -48,7 +48,7 @@ levels = set() for part in level_str.split(','): part = part.strip() - if re.match('\d+..\d+', part): + if re.match(r'\d+..\d+', part): from_level, to_level = part.split('..') levels.update(list(range(int(from_level), int(to_level) + 1))) else: diff -Nru mapproxy-1.12.0/mapproxy/script/util.py mapproxy-1.13.0/mapproxy/script/util.py --- mapproxy-1.12.0/mapproxy/script/util.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/script/util.py 2020-11-19 08:53:21.000000000 +0000 @@ -155,7 +155,7 @@ if ':' in address: host, port = address.split(':', 1) port = int(port) - elif re.match('^\d+$', address): + elif re.match(r'^\d+$', address): host = default[0] port = int(address) else: @@ -387,4 +387,4 @@ commands[command]['func'](args) if __name__ == '__main__': - main() \ No newline at end of file + main() diff -Nru mapproxy-1.12.0/mapproxy/seed/config.py mapproxy-1.13.0/mapproxy/seed/config.py --- mapproxy-1.12.0/mapproxy/seed/config.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/seed/config.py 2020-11-19 08:53:21.000000000 +0000 @@ -58,7 +58,7 @@ else: errors, informal_only = validate_seed_conf(conf) for error in errors: - log.warn(error) + log.warning(error) if not informal_only: raise SeedConfigurationError('invalid configuration') seed_conf = SeedingConfiguration(conf, mapproxy_conf=mapproxy_conf) diff -Nru mapproxy-1.12.0/mapproxy/seed/seeder.py mapproxy-1.13.0/mapproxy/seed/seeder.py --- mapproxy-1.12.0/mapproxy/seed/seeder.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/seed/seeder.py 2020-11-19 08:53:21.000000000 +0000 @@ -43,9 +43,13 @@ CONTAINS = -1 INTERSECTS = 1 -# do not use multiprocessing on windows, it blows -# no lambdas, no anonymous functions/classes, no base_config(), etc. -if sys.platform == 'win32': +# Decide whether to use multiprocessing or threading. multiprocessing should be faster but +# it is not well supported on all platforms. Especially regarding lambdas and anonymous +# function/classes which are used in proj.py for example. +# +# Since Python 3.8, MacOS uses a non-forking start method for multiprocessing which +# inhibits similar restrictions to Windows. +if sys.platform == 'win32' or (sys.platform == 'darwin' and sys.version_info >= (3, 8)): import threading proc_class = threading.Thread queue_class = Queue.Queue @@ -83,7 +87,7 @@ alive = True break if not alive: - log.warn('no workers left, stopping') + log.warning('no workers left, stopping') raise SeedInterrupted continue else: diff -Nru mapproxy-1.12.0/mapproxy/service/demo.py mapproxy-1.13.0/mapproxy/service/demo.py --- mapproxy-1.12.0/mapproxy/service/demo.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/service/demo.py 2020-11-19 08:53:21.000000000 +0000 @@ -91,30 +91,35 @@ elif 'wmts_layer' in req.args: demo = self._render_wmts_template('demo/wmts_demo.html', req) elif 'wms_capabilities' in req.args: - url = '%s/service?REQUEST=GetCapabilities'%(req.script_url) - capabilities = urllib2.urlopen(url) + internal_url = '%s/service?REQUEST=GetCapabilities'%(req.server_script_url) + url = internal_url.replace(req.server_script_url, req.script_url) + capabilities = urllib2.urlopen(internal_url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'WMS', url) elif 'wmsc_capabilities' in req.args: - url = '%s/service?REQUEST=GetCapabilities&tiled=true'%(req.script_url) - capabilities = urllib2.urlopen(url) + internal_url = '%s/service?REQUEST=GetCapabilities&tiled=true'%(req.server_script_url) + url = internal_url.replace(req.server_script_url, req.script_url) + capabilities = urllib2.urlopen(internal_url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'WMS-C', url) elif 'wmts_capabilities_kvp' in req.args: - url = '%s/service?REQUEST=GetCapabilities&SERVICE=WMTS' % (req.script_url) - capabilities = urllib2.urlopen(url) + internal_url = '%s/service?REQUEST=GetCapabilities&SERVICE=WMTS' % (req.server_script_url) + url = internal_url.replace(req.server_script_url, req.script_url) + capabilities = urllib2.urlopen(internal_url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'WMTS', url) elif 'wmts_capabilities' in req.args: - url = '%s/wmts/1.0.0/WMTSCapabilities.xml' % (req.script_url) - capabilities = urllib2.urlopen(url) + internal_url = '%s/wmts/1.0.0/WMTSCapabilities.xml' % (req.server_script_url) + url = internal_url.replace(req.server_script_url, req.script_url) + capabilities = urllib2.urlopen(internal_url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'WMTS', url) elif 'tms_capabilities' in req.args: if 'layer' in req.args and 'srs' in req.args: # prevent dir traversal (seems it's not possible with urllib2, but better safe then sorry) layer = req.args['layer'].replace('..', '') srs = req.args['srs'].replace('..', '') - url = '%s/tms/1.0.0/%s/%s'%(req.script_url, layer, srs) + internal_url = '%s/tms/1.0.0/%s/%s'%(req.server_script_url, layer, srs) else: - url = '%s/tms/1.0.0/'%(req.script_url) - capabilities = urllib2.urlopen(url) + internal_url = '%s/tms/1.0.0/'%(req.server_script_url) + capabilities = urllib2.urlopen(internal_url) + url = internal_url.replace(req.server_script_url, req.script_url) demo = self._render_capabilities_template('demo/capabilities_demo.html', capabilities, 'TMS', url) elif req.path == '/demo/': demo = self._render_template('demo/demo.html') diff -Nru mapproxy-1.12.0/mapproxy/service/template_helper.py mapproxy-1.13.0/mapproxy/service/template_helper.py --- mapproxy-1.12.0/mapproxy/service/template_helper.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/service/template_helper.py 2020-11-19 08:53:21.000000000 +0000 @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cgi import escape from mapproxy.template import bunch +from mapproxy.compat.modules import escape __all__ = ['escape', 'indent', 'bunch', 'wms100format', 'wms100info_format', 'wms111metadatatype'] diff -Nru mapproxy-1.12.0/mapproxy/service/templates/demo/capabilities_demo.html mapproxy-1.13.0/mapproxy/service/templates/demo/capabilities_demo.html --- mapproxy-1.12.0/mapproxy/service/templates/demo/capabilities_demo.html 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/service/templates/demo/capabilities_demo.html 2020-11-19 08:53:21.000000000 +0000 @@ -1,5 +1,5 @@ {{py: -import cgi +from mapproxy.compat.modules import escape import textwrap wrapper = textwrap.TextWrapper(replace_whitespace=False, width=90, @@ -12,7 +12,7 @@ {{url}}
 {{for line in capabilities}}
-{{cgi.escape(wrapper.fill(line.decode('utf8')))}}
+{{escape(wrapper.fill(line.decode('utf8')))}}
 {{endfor}}
             
diff -Nru mapproxy-1.12.0/mapproxy/service/templates/demo/tms_demo.html mapproxy-1.13.0/mapproxy/service/templates/demo/tms_demo.html --- mapproxy-1.12.0/mapproxy/service/templates/demo/tms_demo.html 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/service/templates/demo/tms_demo.html 2020-11-19 08:53:21.000000000 +0000 @@ -1,5 +1,5 @@ {{py: -import cgi +from mapproxy.compat.modules import escape import textwrap wrapper = textwrap.TextWrapper(replace_whitespace=False, width=90, @@ -76,6 +76,6 @@

JavaScript code

 {{for line in jscript_openlayers().split('\n')}}
-{{cgi.escape(wrapper.fill(line))}}
+{{escape(wrapper.fill(line))}}
 {{endfor}}
-            
\ No newline at end of file + diff -Nru mapproxy-1.12.0/mapproxy/service/templates/demo/wms_demo.html mapproxy-1.13.0/mapproxy/service/templates/demo/wms_demo.html --- mapproxy-1.12.0/mapproxy/service/templates/demo/wms_demo.html 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/service/templates/demo/wms_demo.html 2020-11-19 08:53:21.000000000 +0000 @@ -1,5 +1,5 @@ {{py: -import cgi +from mapproxy.compat.modules import escape import textwrap wrapper = textwrap.TextWrapper(replace_whitespace=False, width=90, @@ -72,6 +72,6 @@

JavaScript code

 {{for line in jscript_openlayers().split('\n')}}
-{{cgi.escape(wrapper.fill(line))}}
+{{escape(wrapper.fill(line))}}
 {{endfor}}
-            
\ No newline at end of file + diff -Nru mapproxy-1.12.0/mapproxy/service/templates/demo/wmts_demo.html mapproxy-1.13.0/mapproxy/service/templates/demo/wmts_demo.html --- mapproxy-1.12.0/mapproxy/service/templates/demo/wmts_demo.html 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/service/templates/demo/wmts_demo.html 2020-11-19 08:53:21.000000000 +0000 @@ -1,5 +1,5 @@ {{py: -import cgi +from mapproxy.compat.modules import escape import textwrap wrapper = textwrap.TextWrapper(replace_whitespace=False, width=90, @@ -70,6 +70,6 @@

JavaScript code

 {{for line in jscript_openlayers().split('\n')}}
-{{cgi.escape(wrapper.fill(line))}}
+{{escape(wrapper.fill(line))}}
 {{endfor}}
-            
\ No newline at end of file + diff -Nru mapproxy-1.12.0/mapproxy/service/wms.py mapproxy-1.13.0/mapproxy/service/wms.py --- mapproxy-1.12.0/mapproxy/service/wms.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/service/wms.py 2020-11-19 08:53:21.000000000 +0000 @@ -640,8 +640,8 @@ return layer, layer_img except SourceError: raise - except MapBBOXError: - raise RequestError('Request too large or invalid BBOX.', request=self.request) + except MapBBOXError as e: + raise RequestError('Request too large or invalid BBOX. (%s)' % e, request=self.request) except MapError as e: raise RequestError('Invalid request: %s' % e.args[0], request=self.request) except TransformationError: diff -Nru mapproxy-1.12.0/mapproxy/service/wmts.py mapproxy-1.13.0/mapproxy/service/wmts.py --- mapproxy-1.12.0/mapproxy/service/wmts.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/service/wmts.py 2020-11-19 08:53:21.000000000 +0000 @@ -62,7 +62,7 @@ for layer in layers.values(): grid = layer.grid if not grid.supports_access_with_origin('nw'): - log.warn("skipping layer '%s' for WMTS, grid '%s' of cache '%s' is not compatible with WMTS", + log.warning("skipping layer '%s' for WMTS, grid '%s' of cache '%s' is not compatible with WMTS", layer.name, grid.name, layer.md['cache_name']) continue if grid.name not in sets: diff -Nru mapproxy-1.12.0/mapproxy/source/tile.py mapproxy-1.13.0/mapproxy/source/tile.py --- mapproxy-1.12.0/mapproxy/source/tile.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/source/tile.py 2020-11-19 08:53:21.000000000 +0000 @@ -78,7 +78,7 @@ resp = self.error_handler.handle(e.response_code, query) if resp: return resp - log.warn('could not retrieve tile: %s', e) + log.warning('could not retrieve tile: %s', e) reraise_exception(SourceError(e.args[0]), sys.exc_info()) class CacheSource(CacheMapLayer): diff -Nru mapproxy-1.12.0/mapproxy/source/wms.py mapproxy-1.13.0/mapproxy/source/wms.py --- mapproxy-1.12.0/mapproxy/source/wms.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/source/wms.py 2020-11-19 08:53:21.000000000 +0000 @@ -97,7 +97,7 @@ resp = self.error_handler.handle(e.response_code, query) if resp: return resp - log.warn('could not retrieve WMS map: %s', e) + log.warning('could not retrieve WMS map: %s', e.full_msg or e) reraise_exception(SourceError(e.args[0]), sys.exc_info()) def _get_map(self, query): @@ -107,8 +107,16 @@ if self.supported_formats and format not in self.supported_formats: format = self.supported_formats[0] if self.supported_srs: - if query.srs not in self.supported_srs: + # srs can be equal while still having a different srs_code (EPSG:3857/900913), make sure to use a supported srs_code + request_srs = None + for srs in self.supported_srs: + if query.srs == srs: + request_srs = srs + break + if request_srs is None: return self._get_transformed(query, format) + if query.srs.srs_code != request_srs.srs_code: + query.srs = request_srs if self.extent and not self.extent.contains(MapExtent(query.bbox, query.srs)): return self._get_sub_query(query, format) resp = self.client.retrieve(query, format) diff -Nru mapproxy-1.12.0/mapproxy/srs.py mapproxy-1.13.0/mapproxy/srs.py --- mapproxy-1.12.0/mapproxy/srs.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/srs.py 2020-11-19 08:53:21.000000000 +0000 @@ -23,7 +23,11 @@ import threading from mapproxy.compat.itertools import izip from mapproxy.compat import string_type +from mapproxy.proj import USE_PROJ4_API +# Old Proj.4 API from mapproxy.proj import Proj, transform, set_datapath +# New Proj API +from mapproxy.proj import CRS, Transformer from mapproxy.config import base_config import logging @@ -78,7 +82,7 @@ _thread_local = threading.local() def SRS(srs_code): _init_proj() - if isinstance(srs_code, _SRS): + if isinstance(srs_code, _srs_impl): return srs_code srs_code = _clean_srs_code(srs_code) @@ -89,14 +93,14 @@ if srs_code in _thread_local.srs_cache: return _thread_local.srs_cache[srs_code] else: - srs = _SRS(srs_code) + srs = _srs_impl(srs_code) _thread_local.srs_cache[srs_code] = srs return srs WEBMERCATOR_EPSG = set(('EPSG:900913', 'EPSG:3857', 'EPSG:102100', 'EPSG:102113')) -class _SRS(object): +class _SRS_Proj4_API(object): # http://trac.openlayers.org/wiki/SphericalMercator proj_init = { 'EPSG:4326': lambda: Proj('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs +over'), @@ -110,6 +114,9 @@ """ This class represents a Spatial Reference System. + + Abstracts transformations between different projections. + Uses the old Proj.4 API, either via pyproj 1 or c-types. """ def __init__(self, srs_code): """ @@ -117,7 +124,7 @@ """ self.srs_code = srs_code - init = _SRS.proj_init.get(srs_code, None) + init = self.proj_init.get(srs_code, None) if init is not None: self.proj = init() else: @@ -159,10 +166,10 @@ parts due to distortions. >>> ['%.3f' % x for x in - ... SRS(4326).transform_bbox_to(SRS(900913), (-180.0, -90.0, 180.0, 90.0))] + ... SRS(4326).transform_bbox_to(SRS(3857), (-180.0, -90.0, 180.0, 90.0))] ['-20037508.343', '-147730762.670', '20037508.343', '147730758.195'] >>> ['%.5f' % x for x in - ... SRS(4326).transform_bbox_to(SRS(900913), (8.2, 53.1, 8.3, 53.2))] + ... SRS(4326).transform_bbox_to(SRS(3857), (8.2, 53.1, 8.3, 53.2))] ['912819.82450', '7001516.67745', '923951.77358', '7020078.53264'] >>> SRS(4326).transform_bbox_to(SRS(4326), (8.25, 53.0, 8.5, 53.75)) (8.25, 53.0, 8.5, 53.75) @@ -174,8 +181,8 @@ transf_pts = self.transform_to(other_srs, points) result = calculate_bbox(transf_pts) - log_proj.debug('transformed from %r to %r (%s -> %s)' % - (self, other_srs, bbox, result)) + log_proj.debug('transformed from %r to %r (%s -> %s)', + self, other_srs, bbox, result) return result @@ -257,6 +264,219 @@ >>> SRS(4326) == SRS(3857) False """ + if isinstance(other, _SRS_Proj4_API): + return self.proj.srs == other.proj.srs + else: + return NotImplemented + + def __ne__(self, other): + """ + >>> SRS(3857) != SRS(3857) + False + >>> SRS(4326) != SRS(900913) + True + """ + equal_result = self.__eq__(other) + if equal_result is NotImplemented: + return NotImplemented + else: + return not equal_result + + def __str__(self): + #pylint: disable-msg=E1101 + return "SRS %s ('%s')" % (self.srs_code, self.proj.srs) + + def __repr__(self): + """ + >>> repr(SRS(4326)) + "SRS('EPSG:4326')" + """ + return "SRS('%s')" % (self.srs_code,) + + def __hash__(self): + return hash(self.srs_code) + + +class _SRS(object): + """ + This class represents a Spatial Reference System. + + Abstracts transformations between different projections. + Uses the new Proj API via pyproj >=2. + """ + def __init__(self, srs_code): + """ + Create a new SRS with the given `srs_code` code. + """ + self.srs_code = srs_code + + if srs_code in WEBMERCATOR_EPSG: + epsg_num = 3857 + elif srs_code == 'CRS:84': + epsg_num = 4326 + else: + epsg_num = get_epsg_num(srs_code) + + self.proj = CRS.from_epsg(epsg_num) + + self._transformers = {} + + def _transformer(self, other_srs): + if other_srs in self._transformers: + return self._transformers[other_srs] + + t = Transformer.from_crs(self.proj, other_srs.proj, always_xy=True) + self._transformers[other_srs] = t + return t + + def transform_to(self, other_srs, points): + """ + :type points: ``(x, y)`` or ``[(x1, y1), (x2, y2), …]`` + + >>> srs1 = SRS(4326) + >>> srs2 = SRS(900913) + >>> [str(round(x, 5)) for x in srs1.transform_to(srs2, (8.22, 53.15))] + ['915046.21432', '7010792.20171'] + >>> srs1.transform_to(srs1, (8.25, 53.5)) + (8.25, 53.5) + >>> [(str(round(x, 5)), str(round(y, 5))) for x, y in + ... srs1.transform_to(srs2, [(8.2, 53.1), (8.22, 53.15), (8.3, 53.2)])] + ... #doctest: +NORMALIZE_WHITESPACE + [('912819.8245', '7001516.67745'), + ('915046.21432', '7010792.20171'), + ('923951.77358', '7020078.53264')] + """ + if self == other_srs: + return points + + + transformer = self._transformer(other_srs) + if isinstance(points[0], (int, float)) and 2 >= len(points) <= 3: + return transformer.transform(*points) + + x = [p[0] for p in points] + y = [p[1] for p in points] + transf_pts = transformer.transform(x, y) + return izip(transf_pts[0], transf_pts[1]) + + def transform_bbox_to(self, other_srs, bbox, with_points=16): + """ + + :param with_points: the number of points to use for the transformation. + A bbox transformation with only two or four points may cut off some + parts due to distortions. + + >>> ['%.3f' % x for x in + ... SRS(4326).transform_bbox_to(SRS(3857), (-180.0, -90.0, 180.0, 90.0))] + ['-20037508.343', '-20037508.343', '20037508.343', '20037508.343'] + >>> ['%.5f' % x for x in + ... SRS(4326).transform_bbox_to(SRS(3857), (8.2, 53.1, 8.3, 53.2))] + ['912819.82450', '7001516.67745', '923951.77358', '7020078.53264'] + >>> SRS(4326).transform_bbox_to(SRS(4326), (8.25, 53.0, 8.5, 53.75)) + (8.25, 53.0, 8.5, 53.75) + """ + if self == other_srs: + return bbox + bbox = bbox + points = generate_envelope_points(bbox, with_points) + transf_pts = list(self.transform_to(other_srs, points)) + result = calculate_bbox(transf_pts) + + log_proj.debug('transformed from %r to %r (%s -> %s)', + self, other_srs, bbox, result) + + # XXX: 3857 is only defined within 85.06 N/S, new Proj returns 'inf' for coords + # outside of these bounds. Adjust bbox for 4326->3857 transformations to the old + # behavior, as this is expected in a few places (WMS layer extents and quite a few + # tests). + if self.srs_code == 'EPSG:4326' and other_srs.srs_code in ('EPSG:3857', 'EPSG:900913'): + minx, miny, maxx, maxy = result + if bbox[0] <= -180.0: + minx = -20037508.342789244 + if bbox[1] <= -85.06: + miny = -20037508.342789244 + if bbox[2] >= 180.0: + maxx = 20037508.342789244 + if bbox[3] >= 85.06: + maxy = 20037508.342789244 + result = (minx, miny, maxx, maxy) + return result + + @property + def is_latlong(self): + """ + >>> SRS(4326).is_latlong + True + >>> SRS(31466).is_latlong + False + """ + return self.proj.is_geographic + + @property + def is_axis_order_ne(self): + """ + Returns `True` if the axis order is North, then East + (i.e. y/x or lat/lon). + + >>> SRS(4326).is_axis_order_ne + True + >>> SRS('CRS:84').is_axis_order_ne + False + >>> SRS(31468).is_axis_order_ne + True + >>> SRS(31463).is_axis_order_ne + False + >>> SRS(25831).is_axis_order_ne + False + """ + if self.srs_code == 'CRS:84': + return False + return self.proj.axis_info[0].direction == 'north' + + @property + def is_axis_order_en(self): + """ + Returns `True` if the axis order is East then North + (i.e. x/y or lon/lat). + """ + return not self.is_axis_order_ne + + + def align_bbox(self, bbox): + """ + Align bbox to reasonable values to prevent errors in transformations. + E.g. transformations from EPSG:4326 with lat=90 or -90 will fail, so + we subtract a tiny delta. + + At the moment only EPSG:4326 bbox will be modifyed. + + >>> bbox = SRS(4326).align_bbox((-180, -90, 180, 90)) + >>> -90 < bbox[1] < -89.99999998 + True + >>> 90 > bbox[3] > 89.99999998 + True + """ + # TODO should not be needed anymore since we transform with +over + # still a few tests depend on the rounding behavior of this + if self.srs_code == 'EPSG:4326': + delta = 0.00000001 + (minx, miny, maxx, maxy) = bbox + if abs(miny - -90.0) < 1e-6: + miny = -90.0 + delta + if abs(maxy - 90.0) < 1e-6: + maxy = 90.0 - delta + bbox = minx, miny, maxx, maxy + return bbox + + def __eq__(self, other): + """ + >>> SRS(4326) == SRS("EpsG:4326") + True + >>> SRS(4326) == SRS("4326") + True + >>> SRS(4326) == SRS(3857) + False + """ if isinstance(other, _SRS): return self.proj.srs == other.proj.srs else: @@ -290,6 +510,15 @@ return hash(self.srs_code) +if USE_PROJ4_API: + _srs_impl = _SRS_Proj4_API + del _SRS +else: + _srs_impl = _SRS + del _SRS_Proj4_API + + + def generate_envelope_points(bbox, n): """ Generates points that form a linestring around a given bbox. @@ -365,6 +594,7 @@ maxy = max(bbox1[3], bbox2[3]) return (minx, miny, maxx, maxy) + def bbox_equals(src_bbox, dst_bbox, x_delta=None, y_delta=None): """ Compares two bbox and checks if they are equal, or nearly equal. @@ -444,6 +674,9 @@ self.supported_srs = supported_srs self.preferred_srs = preferred_srs or PreferredSrcSRS() + def __iter__(self): + return iter(self.supported_srs) + def __contains__(self, srs): return srs in self.supported_srs diff -Nru mapproxy-1.12.0/mapproxy/test/helper.py mapproxy-1.13.0/mapproxy/test/helper.py --- mapproxy-1.12.0/mapproxy/test/helper.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/helper.py 2020-11-19 08:53:21.000000000 +0000 @@ -209,9 +209,9 @@ 'barzing1' """ if isinstance(data, bytes): - return re.sub(b'\s+', b'', data) + return re.sub(br'\s+', b'', data) else: - return re.sub('\s+', '', data) + return re.sub(r'\s+', '', data) @contextmanager diff -Nru mapproxy-1.12.0/mapproxy/test/http.py mapproxy-1.13.0/mapproxy/test/http.py --- mapproxy-1.12.0/mapproxy/test/http.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/http.py 2020-11-19 08:53:21.000000000 +0000 @@ -15,10 +15,10 @@ from __future__ import print_function + import re import threading import sys -import cgi import socket import errno import time @@ -26,7 +26,7 @@ from contextlib import contextmanager from mapproxy.util.py import reraise from mapproxy.compat import iteritems, PY2 -from mapproxy.compat.modules import urlparse +from mapproxy.compat.modules import urlparse, parse_qsl if PY2: from cStringIO import StringIO else: @@ -313,7 +313,7 @@ return True -numbers_only = re.compile('^-?\d+\.\d+(,-?\d+\.\d+)*$') +numbers_only = re.compile(r'^-?\d+\.\d+(,-?\d+\.\d+)*$') def query_eq(expected, actual): """ @@ -418,7 +418,7 @@ d = {} if '?' in query: query = query.split('?', 1)[-1] - for key, value in cgi.parse_qsl(query): + for key, value in parse_qsl(query): d[key.lower()] = value return d @@ -480,3 +480,8 @@ def basic_auth_value(username, password): return base64.b64encode(('%s:%s' % (username, password)).encode('utf-8')) + +def assert_no_cache(resp): + assert resp.headers["Pragma"] == "no-cache" + assert resp.headers["Expires"] == "-1" + assert resp.cache_control.no_store == True diff -Nru mapproxy-1.12.0/mapproxy/test/system/fixture/cgi.py mapproxy-1.13.0/mapproxy/test/system/fixture/cgi.py --- mapproxy-1.12.0/mapproxy/test/system/fixture/cgi.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/fixture/cgi.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -#! /usr/bin/env python - -""" -CGI script that returns a red 256x256 PNG file. -""" - -if __name__ == '__main__': - import sys - if sys.version_info[0] == 2: - w = sys.stdout.write - else: - w = sys.stdout.buffer.write - w(b"Content-type: image/png\r\n") - w(b"\r\n") - - w(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\x01\x03\x00\x00\x00f\xbc:%\x00\x00\x00\x06PLTE\xff\x00\x00\x00\x00\x00A\xa3\x12\x03\x00\x00\x00\x1fIDATx\x9c\xed\xc1\x01\r\x00\x00\x00\xc2\xa0\xf7Om\x0e7\xa0\x00\x00\x00\x00\x00\x00\x00\x00\xbe\r!\x00\x00\x01\xf1g!\xee\x00\x00\x00\x00IEND\xaeB`\x82') \ No newline at end of file diff -Nru mapproxy-1.12.0/mapproxy/test/system/fixture/layer.yaml mapproxy-1.13.0/mapproxy/test/system/fixture/layer.yaml --- mapproxy-1.12.0/mapproxy/test/system/fixture/layer.yaml 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/fixture/layer.yaml 2020-11-19 08:53:21.000000000 +0000 @@ -1,4 +1,6 @@ globals: + http: + hide_error_details: false cache: base_dir: cache_data/ meta_size: [1, 1] diff -Nru mapproxy-1.12.0/mapproxy/test/system/fixture/mapserver.yaml mapproxy-1.13.0/mapproxy/test/system/fixture/mapserver.yaml --- mapproxy-1.12.0/mapproxy/test/system/fixture/mapserver.yaml 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/fixture/mapserver.yaml 2020-11-19 08:53:21.000000000 +0000 @@ -19,5 +19,5 @@ req: map: ./foo.map mapserver: - binary: ./cgi.py + binary: ./minimal_cgi.py working_dir: ./tmp diff -Nru mapproxy-1.12.0/mapproxy/test/system/fixture/minimal_cgi.py mapproxy-1.13.0/mapproxy/test/system/fixture/minimal_cgi.py --- mapproxy-1.12.0/mapproxy/test/system/fixture/minimal_cgi.py 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/fixture/minimal_cgi.py 2020-11-19 08:53:21.000000000 +0000 @@ -0,0 +1,16 @@ +#! /usr/bin/env python + +""" +CGI script that returns a red 256x256 PNG file. +""" + +if __name__ == '__main__': + import sys + if sys.version_info[0] == 2: + w = sys.stdout.write + else: + w = sys.stdout.buffer.write + w(b"Content-type: image/png\r\n") + w(b"\r\n") + + w(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\x01\x03\x00\x00\x00f\xbc:%\x00\x00\x00\x06PLTE\xff\x00\x00\x00\x00\x00A\xa3\x12\x03\x00\x00\x00\x1fIDATx\x9c\xed\xc1\x01\r\x00\x00\x00\xc2\xa0\xf7Om\x0e7\xa0\x00\x00\x00\x00\x00\x00\x00\x00\xbe\r!\x00\x00\x01\xf1g!\xee\x00\x00\x00\x00IEND\xaeB`\x82') \ No newline at end of file diff -Nru mapproxy-1.12.0/mapproxy/test/system/fixture/seed_mapproxy.yaml mapproxy-1.13.0/mapproxy/test/system/fixture/seed_mapproxy.yaml --- mapproxy-1.12.0/mapproxy/test/system/fixture/seed_mapproxy.yaml 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/fixture/seed_mapproxy.yaml 2020-11-19 08:53:21.000000000 +0000 @@ -1,4 +1,7 @@ globals: + http: + hide_error_details: false + cache: base_dir: './cache' caches: @@ -33,4 +36,4 @@ type: wms req: url: http://localhost:42423/service? - layers: baz \ No newline at end of file + layers: baz diff -Nru mapproxy-1.12.0/mapproxy/test/system/fixture/source_errors.yaml mapproxy-1.13.0/mapproxy/test/system/fixture/source_errors.yaml --- mapproxy-1.12.0/mapproxy/test/system/fixture/source_errors.yaml 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/fixture/source_errors.yaml 2020-11-19 08:53:21.000000000 +0000 @@ -1,4 +1,6 @@ globals: + http: + hide_error_details: false cache: base_dir: ./cache_data/ meta_size: [1, 1] diff -Nru mapproxy-1.12.0/mapproxy/test/system/test_mapserver.py mapproxy-1.13.0/mapproxy/test/system/test_mapserver.py --- mapproxy-1.12.0/mapproxy/test/system/test_mapserver.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/test_mapserver.py 2020-11-19 08:53:21.000000000 +0000 @@ -45,12 +45,12 @@ @pytest.fixture(scope="class") def additional_files(self, base_dir): shutil.copy( - os.path.join(os.path.dirname(__file__), "fixture", "cgi.py"), + os.path.join(os.path.dirname(__file__), "fixture", "minimal_cgi.py"), base_dir.strpath, ) os.chmod( - base_dir.join("cgi.py").strpath, stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR + base_dir.join("minimal_cgi.py").strpath, stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR ) base_dir.join("tmp").mkdir() diff -Nru mapproxy-1.12.0/mapproxy/test/system/test_source_errors.py mapproxy-1.13.0/mapproxy/test/system/test_source_errors.py --- mapproxy-1.12.0/mapproxy/test/system/test_source_errors.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/test_source_errors.py 2020-11-19 08:53:21.000000000 +0000 @@ -25,7 +25,10 @@ img_from_buf, assert_colors_equal, ) -from mapproxy.test.http import mock_httpd +from mapproxy.test.http import ( + assert_no_cache, + mock_httpd +) from mapproxy.test.system import SysTest from mapproxy.test.system.test_wms import is_111_exception @@ -266,7 +269,7 @@ with mock_httpd(("localhost", 42423), expected_req): resp = app.get(self.common_map_req) - assert "Cache-Control" not in resp.headers + assert_no_cache(resp) assert resp.content_type == "application/vnd.ogc.se_xml" assert b"500" in resp.body @@ -348,8 +351,7 @@ with mock_httpd(("localhost", 42423), expected_req): resp = app.get(self.common_tile_req, status=500) - assert "Cache-Control" not in resp.headers - # no assert_no_cache(resp): returns XML exception that bypasses cache control setting + assert_no_cache(resp) assert resp.content_type == "text/plain" assert b"500" in resp.body @@ -373,9 +375,3 @@ assert resp.content_type == "image/png" img = img_from_buf(resp.body) assert img.getcolors() == [(256 * 256, (100, 50, 50))] - - -def assert_no_cache(resp): - assert resp.headers["Pragma"] == "no-cache" - assert resp.headers["Expires"] == "-1" - assert resp.cache_control.no_store == True diff -Nru mapproxy-1.12.0/mapproxy/test/system/test_wmsc.py mapproxy-1.13.0/mapproxy/test/system/test_wmsc.py --- mapproxy-1.12.0/mapproxy/test/system/test_wmsc.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/test_wmsc.py 2020-11-19 08:53:21.000000000 +0000 @@ -26,6 +26,7 @@ ) from mapproxy.test.image import is_jpeg from mapproxy.test.helper import validate_with_dtd +from mapproxy.test.http import assert_no_cache from mapproxy.test.system.test_wms import is_111_exception from mapproxy.test.system import SysTest @@ -99,17 +100,17 @@ def test_get_tile_wrong_bbox(self, app): self.common_map_req.params.bbox = "-20037508,0.0,200000.0,20037508" resp = app.get(str(self.common_map_req) + "&tiled=true") - assert "Cache-Control" not in resp.headers + assert_no_cache(resp) is_111_exception(resp.lxml, re_msg=".*invalid bbox") def test_get_tile_wrong_fromat(self, app): self.common_map_req.params.format = "image/png" resp = app.get(str(self.common_map_req) + "&tiled=true") - assert "Cache-Control" not in resp.headers + assert_no_cache(resp) is_111_exception(resp.lxml, re_msg="Invalid request: invalid.*format.*jpeg") def test_get_tile_wrong_size(self, app): self.common_map_req.params.size = (256, 255) resp = app.get(str(self.common_map_req) + "&tiled=true") - assert "Cache-Control" not in resp.headers + assert_no_cache(resp) is_111_exception(resp.lxml, re_msg="Invalid request: invalid.*size.*256x256") diff -Nru mapproxy-1.12.0/mapproxy/test/system/test_wms.py mapproxy-1.13.0/mapproxy/test/system/test_wms.py --- mapproxy-1.12.0/mapproxy/test/system/test_wms.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/test_wms.py 2020-11-19 08:53:21.000000000 +0000 @@ -560,7 +560,7 @@ def test_get_map_broken_bbox(self, app): url = ( - """/service?VERSION=1.1.11&REQUEST=GetMap&SRS=EPSG:31468&BBOX=-10000855.0573254,2847125.18913603,-9329367.42767611,4239924.78564583&WIDTH=130&HEIGHT=62&LAYERS=wms_cache&STYLES=&FORMAT=image/png&TRANSPARENT=TRUE""" + """/service?VERSION=1.1.11&REQUEST=GetMap&SRS=EPSG:31468&BBOX=-20000855.0573254,2847125.18913603,-19329367.42767611,4239924.78564583&WIDTH=130&HEIGHT=62&LAYERS=wms_cache&STYLES=&FORMAT=image/png&TRANSPARENT=TRUE""" ) resp = app.get(url) is_111_exception(resp.lxml, "Could not transform BBOX: Invalid result.") diff -Nru mapproxy-1.12.0/mapproxy/test/system/test_wms_srs_extent.py mapproxy-1.13.0/mapproxy/test/system/test_wms_srs_extent.py --- mapproxy-1.12.0/mapproxy/test/system/test_wms_srs_extent.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/system/test_wms_srs_extent.py 2020-11-19 08:53:21.000000000 +0000 @@ -56,8 +56,10 @@ [0.0, 3500000.0, 1000000.0, 8500000.0] ) - assert bbox_srs_from_boundingbox(bboxs["EPSG:3857"]) == pytest.approx( - [-20037508.3428, -147730762.670, 20037508.3428, 147730758.195] + assert bbox_srs_from_boundingbox(bboxs["EPSG:3857"]) in ( + # world BBOX is transformed differently depending on PROJ version + pytest.approx([-20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428]), + pytest.approx([-20037508.3428, -147730762.67, 20037508.3428, 147730762.67]), ) assert bbox_srs_from_boundingbox(bboxs["EPSG:4326"]) == pytest.approx( [-180.0, -90.0, 180.0, 90.0] diff -Nru mapproxy-1.12.0/mapproxy/test/unit/test_cache_couchdb.py mapproxy-1.13.0/mapproxy/test/unit/test_cache_couchdb.py --- mapproxy-1.12.0/mapproxy/test/unit/test_cache_couchdb.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/unit/test_cache_couchdb.py 2020-11-19 08:53:21.000000000 +0000 @@ -111,4 +111,4 @@ assert doc['coord'][1] == pytest.approx(-79.17133464081945) assert doc['coord_webmerc'][0] == pytest.approx(-5009377.085697311) assert doc['coord_webmerc'][1] == pytest.approx(-15028131.25709193) - assert re.match('20\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ', doc['datetime']), doc['datetime'] + assert re.match(r'20\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ', doc['datetime']), doc['datetime'] diff -Nru mapproxy-1.12.0/mapproxy/test/unit/test_client_cgi.py mapproxy-1.13.0/mapproxy/test/unit/test_client_cgi.py --- mapproxy-1.12.0/mapproxy/test/unit/test_client_cgi.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/unit/test_client_cgi.py 2020-11-19 08:53:21.000000000 +0000 @@ -71,7 +71,7 @@ shutil.rmtree(self.script_dir) def create_script(self, script=TEST_CGI_SCRIPT, executable=True): - script_file = os.path.join(self.script_dir, 'cgi.py') + script_file = os.path.join(self.script_dir, 'minimal_cgi.py') with open(script_file, 'wb') as f: f.write(script) if executable: diff -Nru mapproxy-1.12.0/mapproxy/test/unit/test_client.py mapproxy-1.13.0/mapproxy/test/unit/test_client.py --- mapproxy-1.12.0/mapproxy/test/unit/test_client.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/unit/test_client.py 2020-11-19 08:53:21.000000000 +0000 @@ -30,6 +30,7 @@ WMS130MapRequest, WMS111FeatureInfoRequest, ) +from mapproxy.source import SourceError from mapproxy.srs import SRS, SupportedSRS from mapproxy.test.helper import assert_re, TempFile from mapproxy.test.http import mock_httpd, query_eq, assert_query_eq, wms_query_eq @@ -91,6 +92,16 @@ else: assert False, 'expected HTTPClientError' + def test_internal_error_hide_error_details(self): + try: + with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/'}, + {'status': '500', 'body': b''})]): + HTTPClient(hide_error_details=True).open(TESTSERVER_URL + '/') + except HTTPClientError as e: + assert_re(e.args[0], r'HTTP Error \(see logs for URL and reason\).') + else: + assert False, 'expected HTTPClientError' + @pytest.mark.online def test_https_untrusted_root(self): if not supports_ssl_default_context: @@ -287,6 +298,26 @@ resp = client.get_tile((0, 1, 2)).source.read() assert resp == b'tile' + +class TestWMSClient(object): + def test_no_image(self, caplog): + try: + with mock_httpd(TESTSERVER_ADDRESS, [({'path': '/service?map=foo&layers=foo&transparent=true&bbox=-200000,-200000,200000,200000&width=512&height=512&srs=EPSG%3A900913&format=image%2Fpng&request=GetMap&version=1.1.1&service=WMS&styles='}, + {'status': '200', 'body': b'x' * 1000, + 'headers': {'content-type': 'application/foo'}, + })]): + req = WMS111MapRequest(url=TESTSERVER_URL + '/service?map=foo', + param={'layers':'foo', 'transparent': 'true'}) + query = MapQuery((-200000, -200000, 200000, 200000), (512, 512), SRS(900913), 'png') + wms = WMSClient(req).retrieve(query, 'png') + except SourceError: + assert len(caplog.record_tuples) == 1 + assert ("'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' [output truncated]" + in caplog.record_tuples[0][2]) + else: + assert False, 'expected no image returned error' + + class TestCombinedWMSClient(object): def setup(self): self.http = MockHTTPClient() diff -Nru mapproxy-1.12.0/mapproxy/test/unit/test_conf_loader.py mapproxy-1.13.0/mapproxy/test/unit/test_conf_loader.py --- mapproxy-1.12.0/mapproxy/test/unit/test_conf_loader.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/unit/test_conf_loader.py 2020-11-19 08:53:21.000000000 +0000 @@ -903,7 +903,7 @@ except ConfigurationError: pass else: - raise False('expected ConfigurationError') + raise Exception('expected ConfigurationError') conf_dict['globals']['image']['formats']['image/jpeg']['encoding_options'] = { @@ -914,7 +914,7 @@ except ConfigurationError: pass else: - raise False('expected ConfigurationError') + raise Exception('expected ConfigurationError') conf_dict['globals']['image']['formats']['image/jpeg']['encoding_options'] = {} @@ -924,7 +924,7 @@ except ConfigurationError: pass else: - raise False('expected ConfigurationError') + raise Exception('expected ConfigurationError') conf_dict['globals']['image']['formats']['image/jpeg']['encoding_options'] = { diff -Nru mapproxy-1.12.0/mapproxy/test/unit/test_conf_validator.py mapproxy-1.13.0/mapproxy/test/unit/test_conf_validator.py --- mapproxy-1.12.0/mapproxy/test/unit/test_conf_validator.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/unit/test_conf_validator.py 2020-11-19 08:53:21.000000000 +0000 @@ -205,6 +205,33 @@ "requested layers 'foo, bar'" ] + def test_tagged_layer_sources_with_layers(self): + conf = self._test_conf(''' + layers: + - name: one + title: One + sources: ['one_source:foo,bar'] + ''') + + errors = validate_references(conf) + assert errors == [ + "Supported layers for source 'one_source' are 'one' but tagged source " + "requested layers 'foo, bar'" + ] + + def test_tagged_layer_sources_without_layers(self): + conf = self._test_conf(''' + layers: + - name: one + title: One + sources: ['one_source:foo,bar'] + ''') + + del conf['sources']['one_source']['req']['layers'] + + errors = validate_references(conf) + assert errors == [] + def test_tagged_source_without_layers(self): conf = self._test_conf(''' caches: diff -Nru mapproxy-1.12.0/mapproxy/test/unit/test_grid.py mapproxy-1.13.0/mapproxy/test/unit/test_grid.py --- mapproxy-1.12.0/mapproxy/test/unit/test_grid.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/unit/test_grid.py 2020-11-19 08:53:21.000000000 +0000 @@ -907,8 +907,7 @@ def test_broken_bbox(self): grid = TileGrid() - # broken request from "ArcGIS Client Using WinInet" - req_bbox = (-10000855.0573254,2847125.18913603,-9329367.42767611,4239924.78564583) + req_bbox = (-20000855.0573254,2847125.18913603,-19329367.42767611,4239924.78564583) try: grid.get_affected_tiles(req_bbox, (256, 256), req_srs=SRS(31467)) except TransformationError: diff -Nru mapproxy-1.12.0/mapproxy/test/unit/test_image.py mapproxy-1.13.0/mapproxy/test/unit/test_image.py --- mapproxy-1.12.0/mapproxy/test/unit/test_image.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/unit/test_image.py 2020-11-19 08:53:21.000000000 +0000 @@ -19,10 +19,9 @@ from io import BytesIO -import PIL import pytest -from mapproxy.compat.image import Image, ImageDraw +from mapproxy.compat.image import Image, ImageDraw, PIL_VERSION from mapproxy.image import ( BlankImageSource, GeoReference, @@ -113,7 +112,7 @@ assert is_tiff(ir.as_buffer(TIFF_FORMAT)) assert is_tiff(ir.as_buffer()) - @pytest.mark.skipif(PIL.PILLOW_VERSION < '6.1.0', reason="Pillow 6.1.0 required GeoTIFF") + @pytest.mark.skipif(PIL_VERSION < '6.1.0', reason="Pillow 6.1.0 required GeoTIFF") def test_tiff_compression(self): def encoded_size(encoding_options): ir = ImageSource(create_debug_img((100, 100)), PNG_FORMAT) @@ -586,7 +585,7 @@ assert tags[TIFF_GEOKEYDIRECTORYTAG][3*4+3] == srs -@pytest.mark.skipif(PIL.PILLOW_VERSION < '6.1.0', reason="Pillow 6.1.0 required GeoTIFF") +@pytest.mark.skipif(PIL_VERSION < '6.1.0', reason="Pillow 6.1.0 required GeoTIFF") @pytest.mark.parametrize("compression", ['jpeg', 'raw', 'tiff_lzw']) class TestGeoTIFF(object): @@ -678,7 +677,7 @@ for e, a in zip( meshes[0][1], [0.0, 0.0, 0.0, 2000.0, 1000.0, 2000.0, 1000.0, 0.0] ): - assert e == pytest.approx(a) + assert e == pytest.approx(a, abs=1e-9) class TestSingleColorImage(object): diff -Nru mapproxy-1.12.0/mapproxy/test/unit/test_seed_cachelock.py mapproxy-1.13.0/mapproxy/test/unit/test_seed_cachelock.py --- mapproxy-1.12.0/mapproxy/test/unit/test_seed_cachelock.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/unit/test_seed_cachelock.py 2020-11-19 08:53:21.000000000 +0000 @@ -23,6 +23,7 @@ @pytest.mark.skipif(sys.platform == "win32", reason="test not supported for Windows") +@pytest.mark.skipif(sys.platform == "darwin" and sys.version_info >= (3, 8), reason="test not supported for MacOS with Python >=3.8") class TestCacheLock(object): @pytest.fixture diff -Nru mapproxy-1.12.0/mapproxy/test/unit/test_srs.py mapproxy-1.13.0/mapproxy/test/unit/test_srs.py --- mapproxy-1.12.0/mapproxy/test/unit/test_srs.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/test/unit/test_srs.py 2020-11-19 08:53:21.000000000 +0000 @@ -18,7 +18,7 @@ import pytest from mapproxy.config import base_config -from mapproxy import srs +from mapproxy import srs, proj from mapproxy.srs import SRS, PreferredSrcSRS, SupportedSRS @@ -59,7 +59,8 @@ srs2 = SRS(srs1) assert srs1 == srs2 - +# proj_data_dir test relies on old Proj4 epsg files. +@pytest.mark.skipif(not proj.USE_PROJ4_API, reason="only for old proj4 lib") class Test_0_ProjDefaultDataPath(object): def test_known_srs(self): @@ -83,6 +84,7 @@ srs.set_datapath(None) base_config().srs.proj_data_dir = None +@pytest.mark.skipif(not proj.USE_PROJ4_API, reason="only for old proj4 lib") @pytest.mark.usefixtures("custom_proj_data_dir") class Test_1_ProjDataPath(object): @@ -154,6 +156,10 @@ assert SRS(4258) not in supported assert SRS(25832) in supported + def test_iter(self, preferred): + supported = SupportedSRS([SRS(4326), SRS(25832)], preferred) + assert [SRS(4326), SRS(25832)] == [srs for srs in supported] + def test_best_srs(self, preferred): supported = SupportedSRS([SRS(4326), SRS(25832)], preferred) assert supported.best_srs(SRS(4326)) == SRS(4326) diff -Nru mapproxy-1.12.0/mapproxy/util/ext/dictspec/validator.py mapproxy-1.13.0/mapproxy/util/ext/dictspec/validator.py --- mapproxy-1.12.0/mapproxy/util/ext/dictspec/validator.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/util/ext/dictspec/validator.py 2020-11-19 08:53:21.000000000 +0000 @@ -168,11 +168,11 @@ if not isinstance(spec, type): spec = type(spec) - match = re.match("", str(spec)) + match = re.match(r"", str(spec)) if match: return match.group(1) - match = re.match("", str(spec)) + match = re.match(r"", str(spec)) if match: return match.group(1).split('.')[-1] diff -Nru mapproxy-1.12.0/mapproxy/util/ext/lockfile.py mapproxy-1.13.0/mapproxy/util/ext/lockfile.py --- mapproxy-1.12.0/mapproxy/util/ext/lockfile.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/util/ext/lockfile.py 2020-11-19 08:53:21.000000000 +0000 @@ -100,8 +100,8 @@ def _lock_file(file): try: fcntl.flock(file.fileno(), _flags) - except IOError: - raise LockError("Couldn't lock %r" % file.name) + except (IOError, OSError) as err: + raise LockError("Couldn't lock {0}, error: {1}".format(file.name, err)) def _unlock_file(file): diff -Nru mapproxy-1.12.0/mapproxy/util/ext/tempita/__init__.py mapproxy-1.13.0/mapproxy/util/ext/tempita/__init__.py --- mapproxy-1.12.0/mapproxy/util/ext/tempita/__init__.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/util/ext/tempita/__init__.py 2020-11-19 08:53:21.000000000 +0000 @@ -32,11 +32,11 @@ import re import sys -import cgi import os import tokenize from io import StringIO, BytesIO from mapproxy.compat import iteritems, PY2, text_type +from mapproxy.compat.modules import escape from mapproxy.util.py import reraise from mapproxy.util.ext.tempita._looper import looper from mapproxy.util.ext.tempita.compat3 import bytes, basestring_, next, is_unicode, coerce_text @@ -437,10 +437,10 @@ if not isinstance(value, basestring_): value = coerce_text(value) if sys.version >= "3" and isinstance(value, bytes): - value = cgi.escape(value.decode('latin1'), 1) + value = escape(value.decode('latin1'), 1) value = value.encode('latin1') else: - value = cgi.escape(value, 1) + value = escape(value, 1) if sys.version < "3": if is_unicode(value): value = value.encode('ascii', 'xmlcharrefreplace') diff -Nru mapproxy-1.12.0/mapproxy/util/ext/wmsparse/parse.py mapproxy-1.13.0/mapproxy/util/ext/wmsparse/parse.py --- mapproxy-1.12.0/mapproxy/util/ext/wmsparse/parse.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/util/ext/wmsparse/parse.py 2020-11-19 08:53:21.000000000 +0000 @@ -53,7 +53,7 @@ def parse_contact(self): elem = self.find(self.tree, 'Service/ContactInformation') - if elem is None or len(elem) is 0: + if elem is None or len(elem) == 0: elem = etree.Element(None) md = dict( person = self.findtext(elem, 'ContactPersonPrimary/ContactPerson'), diff -Nru mapproxy-1.12.0/mapproxy/util/ext/wmsparse/util.py mapproxy-1.13.0/mapproxy/util/ext/wmsparse/util.py --- mapproxy-1.12.0/mapproxy/util/ext/wmsparse/util.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/util/ext/wmsparse/util.py 2020-11-19 08:53:21.000000000 +0000 @@ -1,6 +1,6 @@ import re -xpath_elem = re.compile('(^|/)([^/]+:)?([^/]+)') +xpath_elem = re.compile(r'(^|/)([^/]+:)?([^/]+)') def resolve_ns(xpath, namespaces, default=None): diff -Nru mapproxy-1.12.0/mapproxy/util/geom.py mapproxy-1.13.0/mapproxy/util/geom.py --- mapproxy-1.12.0/mapproxy/util/geom.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/util/geom.py 2020-11-19 08:53:21.000000000 +0000 @@ -97,7 +97,7 @@ for p in geom: polygons.append(p) else: - log_config.warn('skipping %s geometry from %s: not a Polygon/MultiPolygon', + log_config.warning('skipping %s geometry from %s: not a Polygon/MultiPolygon', geom.type, datasource) except OGRShapeReaderError as ex: raise CoverageReadError(ex) @@ -139,7 +139,7 @@ elif t in ('Polygon', 'MultiPolygon'): geometries.append(geojson) else: - log_config.warn('skipping feature of type %s from %s: not a Polygon/MultiPolygon', + log_config.warning('skipping feature of type %s from %s: not a Polygon/MultiPolygon', t, datasource) polygons = [] @@ -151,7 +151,7 @@ for p in geom: polygons.append(p) else: - log_config.warn('ignoring non-polygon geometry (%s) from %s', + log_config.warning('ignoring non-polygon geometry (%s) from %s', geom.type, datasource) return polygons @@ -168,7 +168,7 @@ for p in geom: polygons.append(p) else: - log_config.warn('ignoring non-polygon geometry (%s) from %s', + log_config.warning('ignoring non-polygon geometry (%s) from %s', geom.type, source) return polygons @@ -277,7 +277,7 @@ tile = tuple(map(int, line.split('/'))) tiles.add(tile) except: - log_config.warn('found error in %s, skipping rest of file', filename) + log_config.warning('found error in %s, skipping rest of file', filename) if os.path.isdir(expire_dir): for root, dirs, files in os.walk(expire_dir): diff -Nru mapproxy-1.12.0/mapproxy/util/lock.py mapproxy-1.13.0/mapproxy/util/lock.py --- mapproxy-1.12.0/mapproxy/util/lock.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/util/lock.py 2020-11-19 08:53:21.000000000 +0000 @@ -111,7 +111,7 @@ if not os.path.exists(lockdir): return if not os.path.isdir(lockdir): - log.warn('lock dir not a directory: %s', lockdir) + log.warning('lock dir not a directory: %s', lockdir) return for entry in os.listdir(lockdir): name = os.path.join(lockdir, entry) @@ -121,7 +121,7 @@ try: os.unlink(name) except IOError as ex: - log.warn('could not remove old lock file %s: %s', name, ex) + log.warning('could not remove old lock file %s: %s', name, ex) except OSError as e: # some one might have removed the file (ENOENT) # or we don't have permissions to remove it (EACCES) diff -Nru mapproxy-1.12.0/mapproxy/wsgiapp.py mapproxy-1.13.0/mapproxy/wsgiapp.py --- mapproxy-1.12.0/mapproxy/wsgiapp.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/mapproxy/wsgiapp.py 2020-11-19 08:53:21.000000000 +0000 @@ -113,7 +113,7 @@ """ The MapProxy WSGI application. """ - handler_path_re = re.compile('^/(\w+)') + handler_path_re = re.compile(r'^/(\w+)') def __init__(self, services, base_config): self.handlers = {} self.base_config = base_config diff -Nru mapproxy-1.12.0/pytest.ini mapproxy-1.13.0/pytest.ini --- mapproxy-1.12.0/pytest.ini 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.13.0/pytest.ini 2020-11-19 08:53:21.000000000 +0000 @@ -0,0 +1,4 @@ +[pytest] +addopts = --doctest-modules +markers = + online: marks tests that require a working internet connection (deselect with '-m "not online"') diff -Nru mapproxy-1.12.0/README.rst mapproxy-1.13.0/README.rst --- mapproxy-1.12.0/README.rst 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/README.rst 2020-11-19 08:53:21.000000000 +0000 @@ -4,7 +4,7 @@ MapProxy is a tile cache, but also offers many new and innovative features like full support for WMS clients. -MapProxy is actively developed and supported by `Omniscale `_, it is released under the Apache Software License 2.0, runs on Unix/Linux and Windows and is easy to install and to configure. +MapProxy is released under the Apache Software License 2.0, runs on Unix/Linux and Windows and is easy to install and to configure. Go to https://mapproxy.org/ for more information. diff -Nru mapproxy-1.12.0/RELEASE.txt mapproxy-1.13.0/RELEASE.txt --- mapproxy-1.12.0/RELEASE.txt 1970-01-01 00:00:00.000000000 +0000 +++ mapproxy-1.13.0/RELEASE.txt 2020-11-19 08:53:21.000000000 +0000 @@ -0,0 +1,45 @@ +Making a new MapProxy release +============================= + +Preparation +----------- + +You will need wheel and twine to build and upload a new release: + + pip install wheel twine + + + +- Update CHANGES.txt with all important changes. Verify version and date in header line. +- Update version in `setup.py` and `doc/conf.py`. +- Commit updates changelog and version and tag the commit with (`git tag 1.12.0`). +- Push the new tag (`git push origin --tags`). + + +Build and upload +---------------- + +Build source tar.gz and wheel. (`egg_info -b "" -D` to remove date suffix from release version). + + python setup.py egg_info -b "" -D sdist + python setup.py egg_info -b "" -D bdist_wheel + + +The new release can be uploaded to https://pypi.org/project/MapProxy/ with twine. You need an account on https://pypi.org and you need to be a collaborator for the MapProxy project. + + twine upload dist/MapProxy-1.12.0.tar.gz + twine upload dist/MapProxy-1.12.0-py2.py3-none-any.whl + +Be aware, you can only upload a file once. If you made a mistake (e.g. files are missing in the tar.gz) then you will need to make a new release with an updated minor version. + + + + +Announce +-------- + + +- Add a new blog article to mapproxy.org with the most important changes +- MapProxy mailing list (copy from previous release mail as a template) +- Twitter (with link to blog) + diff -Nru mapproxy-1.12.0/requirements-tests.txt mapproxy-1.13.0/requirements-tests.txt --- mapproxy-1.12.0/requirements-tests.txt 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/requirements-tests.txt 2020-11-19 08:53:21.000000000 +0000 @@ -1,66 +1,78 @@ -Jinja2==2.10.1 -MarkupSafe==1.0 -Pillow==5.1.0 -PyYAML==5.1 -Shapely==1.6.4.post1 -WebOb==1.8.1 -WebTest==2.0.29 -Werkzeug==0.14.1 -asn1crypto==0.24.0 -atomicwrites==1.1.5 -attrs==18.1.0 -aws-xray-sdk==0.95 -backports.ssl-match-hostname==3.5.0.1 -backports.tempfile==1.0 -backports.weakref==1.0.post1 -basho-erlastic==2.1.1 -beautifulsoup4==4.6.0 -boto==2.48.0 -boto3==1.7.27 -botocore==1.10.27 -certifi==2018.4.16 -cffi==1.11.5 +Jinja2==2.11.2 +MarkupSafe==1.1.1 +Pillow==6.2.2;python_version<"3.0" +Pillow==7.2.0;python_version>="3.0" +PyYAML==5.3.1 +Shapely==1.7.0 +WebOb==1.8.6 +WebTest==2.0.35 +attrs==19.3.0 +aws-sam-translator==1.26.0 +aws-xray-sdk==2.6.0 +beautifulsoup4==4.9.1 +boto3==1.14.46 +boto==2.49.0 +botocore==1.17.46 +certifi==2020.6.20 +cffi==1.14.2 +cfn-lint==0.35.0 chardet==3.0.4 -cookies==2.2.1 -coverage==4.5.1 -cryptography==2.3.0 -docker==3.3.0 -docker-pycreds==0.2.3 -docutils==0.14 -enum-compat==0.0.2 -enum34==1.1.6 -funcsigs==1.0.2 -futures==3.2.0;python_version<"3.0" -greenlet==0.4.13 -httpretty==0.9.4 -idna==2.6 -ipaddress==1.0.22 -jmespath==0.9.3 -jsondiff==1.1.1 -jsonpickle==0.9.6 -lxml==4.2.1 -mock==2.0.0 -more-itertools==4.2.0 -moto==1.3.3 -olefile==0.45.1 -pbr==4.0.3 -pluggy==0.6.0 -protobuf==2.6.1 -py==1.5.3 -pyOpenSSL==18.0.0 -pyaml==17.12.1 -pycparser==2.18 -pytest==3.6.0 -python-dateutil==2.6.1 -pytz==2018.4 -redis==2.10.6 -requests==2.20.0 -responses==0.9.0 +cryptography==3.0 +decorator==4.4.2 +docker==4.3.0 +docutils==0.15.2 +ecdsa==0.14.1 +future==0.18.2 +idna==2.8 +importlib-metadata==1.7.0 +iniconfig==1.0.1 +jmespath==0.10.0 +jsondiff==1.1.2 +jsonpatch==1.26 +jsonpickle==1.4.1 +jsonpointer==2.0 +jsonschema==3.2.0 +junit-xml==1.9 +lxml==4.5.2 +mock==3.0.5;python_version<"3.6" +mock==4.0.2;python_version>="3.6" +more-itertools==5.0.0;python_version<"3.0" +more-itertools==8.4.0;python_version>="3.0" +moto==1.3.14 +networkx==2.2;python_version<"3.0" +networkx==2.4;python_version>="3.0" +packaging==20.4 +pluggy==0.13.1 +py==1.9.0 +pyasn1==0.4.8 +pycparser==2.20 +pyparsing==2.2.2;python_version<"3.0" +pyparsing==2.4.7;python_version>="3.0" +pyproj==2.2.2;python_version<"3.0" +pyproj==2.6.1.post1;python_version>="3.0" +pyrsistent==0.16.0 +pytest==4.6.11;python_version<"3.0" +pytest==6.0.1;python_version>="3.0" +python-dateutil==2.8.1 +python-jose==3.2.0 +pytz==2020.1 +redis==3.5.3 +requests==2.24.0 +responses==0.10.16 riak==2.7.0 -s3transfer==0.1.13 -six==1.11.0 -urllib3==1.24.2 -waitress==1.1.0 -websocket-client==0.47.0 -wrapt==1.10.11 -xmltodict==0.11.0 +rsa==4.5;python_version<"3.0" +rsa==4.6;python_version>="3.0" +s3transfer==0.3.3 +six==1.15.0 +soupsieve==1.9.6;python_version<"3.0" +soupsieve==2.0.1;python_version>="3.0" +sshpubkeys==3.1.0 +toml==0.10.1 +urllib3==1.25.10 +waitress==1.4.4 +websocket-client==0.57.0 +werkzeug==1.0.1 +wrapt==1.12.1 +xmltodict==0.12.0 +zipp==1.2.0;python_version<"3.6" +zipp==3.1.0;python_version>="3.6" diff -Nru mapproxy-1.12.0/setup.py mapproxy-1.13.0/setup.py --- mapproxy-1.12.0/setup.py 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/setup.py 2020-11-19 08:53:21.000000000 +0000 @@ -37,7 +37,7 @@ readme = open('README.rst').read() changes = ['Changes\n-------\n'] - version_line_re = re.compile('^\d\.\d+\.\d+\S*\s20\d\d-\d\d-\d\d') + version_line_re = re.compile(r'^\d\.\d+\.\d+\S*\s20\d\d-\d\d-\d\d') for line in open('CHANGES.txt'): if version_line_re.match(line): if changelog_releases == 0: @@ -54,7 +54,7 @@ setup( name='MapProxy', - version="1.12.0", + version="1.13.0", description='An accelerating proxy for tile and web map services', long_description=long_description(7), author='Oliver Tonnhofer', @@ -76,10 +76,11 @@ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Internet :: Proxy Servers", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Scientific/Engineering :: GIS", diff -Nru mapproxy-1.12.0/.travis.yml mapproxy-1.13.0/.travis.yml --- mapproxy-1.12.0/.travis.yml 2019-08-30 07:34:08.000000000 +0000 +++ mapproxy-1.13.0/.travis.yml 2020-11-19 08:53:21.000000000 +0000 @@ -2,10 +2,10 @@ python: - "2.7" - - "3.4" - "3.5" - "3.6" - "3.7" + - "3.8" services: - couchdb @@ -13,9 +13,6 @@ addons: apt: - sources: - - sourceline: 'deb https://packagecloud.io/basho/riak/ubuntu/ xenial main' - key_url: 'https://packagecloud.io/basho/riak/gpgkey' packages: - proj-bin - libgeos-dev @@ -29,35 +26,28 @@ - libfreetype6-dev - protobuf-compiler - libprotoc-dev - - riak env: global: - MAPPROXY_TEST_COUCHDB=http://127.0.0.1:5984 - MAPPROXY_TEST_REDIS=127.0.0.1:6379 - - MAPPROXY_TEST_RIAK_HTTP=http://localhost:8098 - - MAPPROXY_TEST_RIAK_PBC=pbc://localhost:8087 - # do not load /etc/boto.cfg with Python 3 incompatible plugin # https://github.com/travis-ci/travis-ci/issues/5246#issuecomment-166460882 - BOTO_CONFIG=/doesnotexist matrix: include: - # Test 2.7 and 3.7 also with latest Pillow version + # Test 2.7 and 3.8 also with latest Pillow version - python: "2.7" env: USE_LATEST_PILLOW=1 - - python: "3.7" + - python: "3.8" env: USE_LATEST_PILLOW=1 cache: directories: - $HOME/.cache/pip -before_install: - - "sudo systemctl start riak" - install: - "pip install -r requirements-tests.txt" - "if [[ $USE_LATEST_PILLOW = '1' ]]; then pip install -U Pillow; fi"