Skip to content

Commit 8ee9486

Browse files
authored
Merge pull request #303 from tybeller/feature/http-verify
feat: Add support for setting SSL verification in http requests
2 parents 2eb3880 + f18efda commit 8ee9486

2 files changed

Lines changed: 65 additions & 30 deletions

File tree

edgar/httpclient.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os
23
import threading
34
from contextlib import asynccontextmanager, contextmanager, nullcontext
45
import httpx
@@ -8,41 +9,52 @@
89

910
log = logging.getLogger(__name__)
1011

11-
PERSISTENT_CLIENT = True # When enabled, httpclient reuses httpx clients rather than creating a single request per client
12+
PERSISTENT_CLIENT = True # When enabled, httpclient reuses httpx clients rather than creating a single request per client
13+
14+
15+
def get_edgar_verify_ssl():
16+
"""
17+
Returns True if using SSL verification on http requests
18+
"""
19+
return os.environ.get("EDGAR_VERIFY_SSL", "true").lower() != "false"
20+
1221

1322
DEFAULT_PARAMS = {
1423
"timeout": edgar_mode.http_timeout,
1524
"limits": edgar_mode.limits,
1625
"default_encoding": "utf-8",
26+
"verify": get_edgar_verify_ssl(),
1727
}
1828

1929
client_factory_class = httpx.Client
2030
asyncclient_factory_class = httpx.AsyncClient
2131

22-
def _client_factory(**kwargs)-> httpx.Client:
32+
33+
def _client_factory(**kwargs) -> httpx.Client:
2334
params = DEFAULT_PARAMS.copy()
2435
params["headers"] = client_headers()
25-
36+
2637
params.update(**kwargs)
27-
38+
2839
return client_factory_class(**params)
2940

41+
3042
def _http_client_manager():
3143
"""When PERSISTENT_CLIENT, creates and reuses a single client. Otherwise, creates a new client per invocation."""
3244
client = None
3345
lock = threading.Lock()
3446

3547
@contextmanager
36-
def _get_client( **kwargs):
48+
def _get_client(**kwargs):
3749
if PERSISTENT_CLIENT:
3850
nonlocal client
3951

4052
if client is None:
4153
with lock:
42-
if client is None:
54+
if client is None:
4355
client = _client_factory(**kwargs)
4456
log.info("Creating new HTTPX Client")
45-
57+
4658
yield client
4759
else:
4860
# Create a new client per request
@@ -62,13 +74,15 @@ def _close_client():
6274

6375
return _get_client, _close_client
6476

77+
6578
http_client, _close_client = _http_client_manager()
6679

80+
6781
@asynccontextmanager
6882
async def async_http_client(client: Optional[httpx.AsyncClient] = None, **kwargs) -> AsyncGenerator[httpx.AsyncClient, None]:
6983
"""
7084
Async callers should create a single client for a group of tasks, rather than creating a single client per task.
71-
85+
7286
Client: Optional parameter to allow code paths that accept optional clients: if a client is passed, this is a no-op and the client isn't closed.
7387
"""
7488

@@ -77,11 +91,12 @@ async def async_http_client(client: Optional[httpx.AsyncClient] = None, **kwargs
7791

7892
params = DEFAULT_PARAMS.copy()
7993
params["headers"] = client_headers()
80-
94+
8195
params.update(**kwargs)
8296
async with asyncclient_factory_class(**params) as client:
8397
yield client
8498

99+
85100
def close_clients():
86101
"""Closes and invalidates existing client sessions."""
87-
_close_client()
102+
_close_client()

tests/test_httprequests.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import httpx
66
import pytest
77

8-
from edgar.httpclient import async_http_client
8+
from edgar.httpclient import async_http_client, get_edgar_verify_ssl
99

1010
from edgar.httprequests import (
1111
get_with_retry,
@@ -16,7 +16,7 @@
1616
TooManyRequestsError,
1717
IdentityNotSetException,
1818
download_file,
19-
download_text_between_tags
19+
download_text_between_tags,
2020
)
2121

2222

@@ -48,16 +48,17 @@ def test_get_with_retry_for_redirect(status_code, monkeypatch):
4848
with patch("httpx.Client.get", return_value=mock_response):
4949
with patch("edgar.httprequests.get_with_retry") as mock_retry:
5050
get_with_retry(url="http://example.com")
51-
mock_retry.assert_called_once_with(url="http://example.com/redirected",
52-
identity=os.environ['EDGAR_IDENTITY'],
53-
headers={'User-Agent': 'Dev Gunning developer-gunning@gmail.com'},
54-
identity_callable=None)
51+
mock_retry.assert_called_once_with(
52+
url="http://example.com/redirected",
53+
identity=os.environ["EDGAR_IDENTITY"],
54+
headers={"User-Agent": "Dev Gunning developer-gunning@gmail.com"},
55+
identity_callable=None,
56+
)
5557

5658

5759
@pytest.mark.asyncio
5860
@pytest.mark.parametrize("status_code", [200, 429])
5961
async def test_get_with_retry_async(status_code):
60-
6162
async with async_http_client() as client:
6263
mock_response = httpx.Response(status_code=status_code)
6364
with patch("httpx.AsyncClient.get", return_value=mock_response):
@@ -84,10 +85,13 @@ async def test_get_with_retry_async_for_redirect(status_code):
8485
with patch("httpx.Client.get", return_value=mock_response):
8586
with patch("edgar.httprequests.get_with_retry") as mock_retry:
8687
get_with_retry(url="http://example.com")
87-
mock_retry.assert_called_once_with(url="http://example.com/redirected",
88-
identity=os.environ['EDGAR_IDENTITY'],
89-
headers={'User-Agent': 'Dev Gunning developer-gunning@gmail.com'},
90-
identity_callable=None)
88+
mock_retry.assert_called_once_with(
89+
url="http://example.com/redirected",
90+
identity=os.environ["EDGAR_IDENTITY"],
91+
headers={"User-Agent": "Dev Gunning developer-gunning@gmail.com"},
92+
identity_callable=None,
93+
)
94+
9195

9296
def test_post_with_retry():
9397
mock_response = httpx.Response(status_code=200)
@@ -139,9 +143,10 @@ def test_identity_from_environment_variable(monkeypatch):
139143
@pytest.mark.asyncio
140144
async def test_get_daily_index_url_async():
141145
async with async_http_client() as client:
142-
143-
urls = ['https://www.sec.gov/Archives/edgar/daily-index/2024/QTR2/form.20240502.idx',
144-
'https://www.sec.gov/Archives/edgar/daily-index/2024/QTR2/form.20240502.idx']
146+
urls = [
147+
"https://www.sec.gov/Archives/edgar/daily-index/2024/QTR2/form.20240502.idx",
148+
"https://www.sec.gov/Archives/edgar/daily-index/2024/QTR2/form.20240502.idx",
149+
]
145150
# Use asyncio to run get_with_retry_async
146151
tasks = [get_with_retry_async(client=client, url=url) for url in urls]
147152
results = await asyncio.gather(*tasks)
@@ -150,18 +155,33 @@ async def test_get_daily_index_url_async():
150155

151156

152157
def test_download_index_file():
153-
xbrl_gz = download_file('https://www.sec.gov/Archives/edgar/full-index/2021/QTR1/xbrl.gz')
158+
xbrl_gz = download_file("https://www.sec.gov/Archives/edgar/full-index/2021/QTR1/xbrl.gz")
154159
assert isinstance(xbrl_gz, bytes)
155160
assert len(xbrl_gz) > 10000
156161

157-
xbrl_idx = download_file('https://www.sec.gov/Archives/edgar/full-index/2021/QTR1/xbrl.idx')
162+
xbrl_idx = download_file("https://www.sec.gov/Archives/edgar/full-index/2021/QTR1/xbrl.idx")
158163
assert isinstance(xbrl_idx, str)
159164

160165

161166
def test_get_text_between_tags():
162-
text = download_text_between_tags(
163-
'https://www.sec.gov/Archives/edgar/data/1009672/000156459018004771/0001564590-18-004771.txt',
164-
'SEC-HEADER')
165-
assert 'ACCESSION NUMBER: 0001564590-18-004771' in text
167+
text = download_text_between_tags("https://www.sec.gov/Archives/edgar/data/1009672/000156459018004771/0001564590-18-004771.txt", "SEC-HEADER")
168+
assert "ACCESSION NUMBER: 0001564590-18-004771" in text
166169
assert text.strip().endswith("77079")
167170

171+
172+
def test_edgar_verify_ssl(monkeypatch):
173+
# True when env variable doesn't exist
174+
monkeypatch.delenv("EDGAR_VERIFY_SSL", raising=False)
175+
assert get_edgar_verify_ssl()
176+
177+
# True when env variable set to true
178+
monkeypatch.setenv("EDGAR_VERIFY_SSL", "true")
179+
assert get_edgar_verify_ssl()
180+
181+
# True when env variable set to undefined value
182+
monkeypatch.setenv("EDGAR_VERIFY_SSL", "unknown")
183+
assert get_edgar_verify_ssl()
184+
185+
# False when env variable set to false
186+
monkeypatch.setenv("EDGAR_VERIFY_SSL", "false")
187+
assert not get_edgar_verify_ssl()

0 commit comments

Comments
 (0)