diff --git a/docs/api/datadir.rst b/docs/api/datadir.rst index 31619712..bfec66d4 100644 --- a/docs/api/datadir.rst +++ b/docs/api/datadir.rst @@ -1,7 +1,7 @@ .. _data_directory: Data Directory -============== +=============== pyproj.datadir.get_data_dir --------------------------- diff --git a/docs/api/index.rst b/docs/api/index.rst index 4ff5f33f..91877892 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -11,6 +11,7 @@ API Documentation proj list datadir + network sync global_context enums diff --git a/docs/api/network.rst b/docs/api/network.rst new file mode 100644 index 00000000..b88eafbe --- /dev/null +++ b/docs/api/network.rst @@ -0,0 +1,10 @@ +.. _network: + +PROJ Network Settings +====================== + + +pyproj.network.set_ca_bundle_path +---------------------------------- + +.. autofunction:: pyproj.network.set_ca_bundle_path diff --git a/docs/history.rst b/docs/history.rst index 15924204..497ac258 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -22,6 +22,7 @@ Change Log * ENH: Added ability to use global context (issue #661) * ENH: Add support for temporal CRS CF coordinate system (issue #672) * BUG: Fix handling of polygon holes when calculating area in Geod (pull #686) +* ENH: Added :func:`pyproj.network.set_ca_bundle_path` (pull #691) 2.6.1 ~~~~~ diff --git a/docs/transformation_grids.rst b/docs/transformation_grids.rst index 3aea7eef..95e9da92 100644 --- a/docs/transformation_grids.rst +++ b/docs/transformation_grids.rst @@ -9,7 +9,7 @@ More information about the data available is located under the PROJ `resource files `__ documentation. -`pyproj` API for managing the :ref:`data_directory` +`pyproj` API for managing the :ref:`data_directory` and :ref:`network`. .. warning:: pyproj 2 includes datumgrid 1.8 in the wheels. pyproj 3 will not include any datum grids. diff --git a/pyproj/__init__.py b/pyproj/__init__.py index 83197b0d..c3cfbbfe 100644 --- a/pyproj/__init__.py +++ b/pyproj/__init__.py @@ -46,6 +46,7 @@ ] import warnings +import pyproj.network from pyproj._datadir import ( # noqa: F401 _pyproj_global_context_initialize, is_global_context_network_enabled, @@ -79,3 +80,5 @@ _pyproj_global_context_initialize() except DataDirError as err: warnings.warn(str(err)) + +pyproj.network.set_ca_bundle_path() diff --git a/pyproj/_network.pyi b/pyproj/_network.pyi new file mode 100644 index 00000000..3caa1766 --- /dev/null +++ b/pyproj/_network.pyi @@ -0,0 +1 @@ +def _set_ca_bundle_path(ca_bundle_path: str) -> None: ... diff --git a/pyproj/_network.pyx b/pyproj/_network.pyx new file mode 100644 index 00000000..c1d1be74 --- /dev/null +++ b/pyproj/_network.pyx @@ -0,0 +1,16 @@ +include "proj.pxi" + +from pyproj.compat import cstrencode + + +def _set_ca_bundle_path(ca_bundle_path): + """ + Sets the path to the CA Bundle used by the `curl` + built into PROJ. + + Parameters + ---------- + ca_bundle_path: str + The path to the CA Bundle. + """ + proj_context_set_ca_bundle_path(NULL, cstrencode(ca_bundle_path)) diff --git a/pyproj/network.py b/pyproj/network.py new file mode 100644 index 00000000..363ac203 --- /dev/null +++ b/pyproj/network.py @@ -0,0 +1,51 @@ +""" +Module for managing the PROJ network settings. +""" +import os +from pathlib import Path +from typing import Union + +import certifi + +from pyproj._network import _set_ca_bundle_path + + +def set_ca_bundle_path(ca_bundle_path: Union[Path, str, bool, None] = None) -> None: + """ + .. versionadded:: 3.0.0 + + Sets the path to the CA Bundle used by the `curl` + built into PROJ. + + Environment variables that can be used with PROJ 7.2+: + + - PROJ_CURL_CA_BUNDLE + - CURL_CA_BUNDLE + - SSL_CERT_FILE + + Parameters + ---------- + ca_bundle_path: Union[Path, str, bool, None], optional + Default is None, which only uses the `certifi` package path as a fallback if + the environment variables are not set. If a path is passed in, then + that will be the path used. If it is set to True, then it will default + to using the path provied by the `certifi` package. If it is set to False, + then it will not set the path. + """ + if ca_bundle_path is False: + return + + env_var_names = ( + "PROJ_CURL_CA_BUNDLE", + "CURL_CA_BUNDLE", + "SSL_CERT_FILE", + ) + if isinstance(ca_bundle_path, (str, Path)): + ca_bundle_path = str(ca_bundle_path) + elif (ca_bundle_path is True) or not any( + env_var_name in os.environ for env_var_name in env_var_names + ): + ca_bundle_path = certifi.where() + + if isinstance(ca_bundle_path, str): + _set_ca_bundle_path(ca_bundle_path) diff --git a/pyproj/proj.pxi b/pyproj/proj.pxi index 74e02b3e..4cb908d5 100644 --- a/pyproj/proj.pxi +++ b/pyproj/proj.pxi @@ -9,6 +9,7 @@ cdef extern from "proj.h": const char *dbPath, const char *const *auxDbPaths, const char* const *options) + void proj_context_set_ca_bundle_path(PJ_CONTEXT *ctx, const char *path); # projCtx has been replaced by PJ_CONTEXT *. # projPJ has been replaced by PJ * diff --git a/setup.py b/setup.py index 69e7ed0e..b3845a5a 100644 --- a/setup.py +++ b/setup.py @@ -166,6 +166,7 @@ def get_extension_modules(): ), Extension("pyproj._datadir", ["pyproj/_datadir.pyx"], **ext_options), Extension("pyproj._list", ["pyproj/_list.pyx"], **ext_options), + Extension("pyproj._network", ["pyproj/_network.pyx"], **ext_options), Extension("pyproj._sync", ["pyproj/_sync.pyx"], **ext_options), ], quiet=True, @@ -244,4 +245,5 @@ def get_long_description(): ext_modules=get_extension_modules(), package_data=get_package_data(), zip_safe=False, # https://mypy.readthedocs.io/en/stable/installed_packages.html + install_requires=["certifi"], ) diff --git a/test/test_network.py b/test/test_network.py new file mode 100644 index 00000000..b83a5b22 --- /dev/null +++ b/test/test_network.py @@ -0,0 +1,49 @@ +import certifi +import pytest +from mock import patch + +from pyproj.network import set_ca_bundle_path + + +@patch.dict("os.environ", {}, clear=True) +@patch("pyproj.network._set_ca_bundle_path") +def test_ca_bundle_path__default(c_set_ca_bundle_path_mock): + set_ca_bundle_path() + c_set_ca_bundle_path_mock.assert_called_with(certifi.where()) + + +@pytest.mark.parametrize( + "env_var", ["PROJ_CURL_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE"] +) +@patch("pyproj.network._set_ca_bundle_path") +def test_ca_bundle_path__always_certifi(c_set_ca_bundle_path_mock, env_var): + with patch.dict("os.environ", {env_var: "/tmp/dummy/path/cacert.pem"}, clear=True): + set_ca_bundle_path(True) + c_set_ca_bundle_path_mock.assert_called_with(certifi.where()) + + +@patch.dict("os.environ", {}, clear=True) +@patch("pyproj.network._set_ca_bundle_path") +def test_ca_bundle_path__skip(c_set_ca_bundle_path_mock): + set_ca_bundle_path(False) + c_set_ca_bundle_path_mock.assert_not_called() + + +@pytest.mark.parametrize( + "env_var", ["PROJ_CURL_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE"] +) +@patch("pyproj.network._set_ca_bundle_path") +def test_ca_bundle_path__env_var_skip(c_set_ca_bundle_path_mock, env_var): + with patch.dict("os.environ", {env_var: "/tmp/dummy/path/cacert.pem"}, clear=True): + set_ca_bundle_path() + c_set_ca_bundle_path_mock.assert_not_called() + + +@pytest.mark.parametrize( + "env_var", ["PROJ_CURL_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE"] +) +@patch("pyproj.network._set_ca_bundle_path") +def test_ca_bundle_path__custom_path(c_set_ca_bundle_path_mock, env_var): + with patch.dict("os.environ", {env_var: "/tmp/dummy/path/cacert.pem"}, clear=True): + set_ca_bundle_path("/my/path/to/cacert.pem") + c_set_ca_bundle_path_mock.assert_called_with("/my/path/to/cacert.pem")