certapi 0.5.1__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {certapi-0.5.1/src/certapi.egg-info → certapi-0.6.0}/PKG-INFO +1 -1
  2. {certapi-0.5.1 → certapi-0.6.0}/pyproject.toml +0 -1
  3. {certapi-0.5.1 → certapi-0.6.0}/setup.py +1 -1
  4. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/acme/AcmeError.py +22 -13
  5. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/acme/http.py +1 -1
  6. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/errors.py +1 -0
  7. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/http/HttpClientBase.py +6 -5
  8. {certapi-0.5.1 → certapi-0.6.0/src/certapi.egg-info}/PKG-INFO +1 -1
  9. {certapi-0.5.1 → certapi-0.6.0}/src/certapi.egg-info/SOURCES.txt +1 -0
  10. {certapi-0.5.1 → certapi-0.6.0}/tests/test_cert_issuer_generic.py +4 -4
  11. certapi-0.6.0/tests/test_http_error_handling.py +140 -0
  12. {certapi-0.5.1 → certapi-0.6.0}/tests/test_keystores.py +5 -3
  13. {certapi-0.5.1 → certapi-0.6.0}/MANIFEST.in +0 -0
  14. {certapi-0.5.1 → certapi-0.6.0}/README.md +0 -0
  15. {certapi-0.5.1 → certapi-0.6.0}/setup.cfg +0 -0
  16. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/__init__.py +0 -0
  17. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/acme/Acme.py +0 -0
  18. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/acme/Challenge.py +0 -0
  19. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/acme/Order.py +0 -0
  20. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/acme/__init__.py +0 -0
  21. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
  22. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
  23. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +0 -0
  24. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/__init__.py +0 -0
  25. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/dns/__init__.py +0 -0
  26. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
  27. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
  28. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
  29. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
  30. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
  31. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
  32. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/client/__init__.py +0 -0
  33. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/client/cert_manager_client.py +0 -0
  34. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/crypto/__init__.py +0 -0
  35. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/crypto/crypto.py +0 -0
  36. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/crypto/crypto_classes.py +0 -0
  37. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/http/__init__.py +0 -0
  38. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/http/types.py +0 -0
  39. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/issuers/AcmeCertIssuer.py +0 -0
  40. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/issuers/SelfCertIssuer.py +0 -0
  41. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/issuers/__init__.py +0 -0
  42. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/issuers/abstract_certissuer.py +0 -0
  43. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
  44. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/keystore/KeyStore.py +0 -0
  45. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
  46. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/keystore/RemoteKeyStore.py +0 -0
  47. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/keystore/SqliteKeyStore.py +0 -0
  48. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/keystore/__init__.py +0 -0
  49. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/manager/__init__.py +0 -0
  50. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/manager/acme_cert_manager.py +0 -0
  51. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/server/__init__.py +0 -0
  52. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/server/api.py +0 -0
  53. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/server/cert_api.py +0 -0
  54. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/server/key_api.py +0 -0
  55. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/util.py +0 -0
  56. {certapi-0.5.1 → certapi-0.6.0}/src/certapi/utils.py +0 -0
  57. {certapi-0.5.1 → certapi-0.6.0}/src/certapi.egg-info/dependency_links.txt +0 -0
  58. {certapi-0.5.1 → certapi-0.6.0}/src/certapi.egg-info/requires.txt +0 -0
  59. {certapi-0.5.1 → certapi-0.6.0}/src/certapi.egg-info/top_level.txt +0 -0
  60. {certapi-0.5.1 → certapi-0.6.0}/tests/test_certs_with_key_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: certapi
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Python Package for managing keys, request SSL certificates from ACME.
5
5
  Home-page: https://github.com/mesudip/certapi
6
6
  Author: Sudip Bhattarai
@@ -9,7 +9,6 @@ testpaths = [
9
9
  ]
10
10
  env_files =[".env", ".test.env"]
11
11
  pythonpath = "src"
12
- timeout = 10
13
12
 
14
13
  [build-system]
15
14
  requires = ["setuptools>=42"]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="certapi",
5
- version="0.5.1",
5
+ version="0.6.0",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -61,30 +61,36 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
61
61
  message = err_detail
62
62
  else:
63
63
  validation_record = validation_record[0]
64
- error["hostname"] = validation_record["hostname"]
64
+ error["hostname"] = validation_record.get("hostname", "unknown")
65
65
  error["dns"] = {
66
- "resolved": validation_record["addressesResolved"],
67
- "used": validation_record["addressUsed"],
66
+ "resolved": validation_record.get("addressesResolved"),
67
+ "used": validation_record.get("addressUsed"),
68
68
  }
69
69
  if err_type == "urn:ietf:params:acme:error:connection":
70
70
  if "Timeout during connect" in err_detail:
71
71
  error["connect"] = {"error": "Timeout"}
72
+ address_used = validation_record.get("addressUsed", "unknown")
73
+ port = str(validation_record.get("port", "unknown"))
74
+ hostname = error.get("hostname", "unknown")
72
75
  message = (
73
- error["hostname"]
76
+ str(hostname)
74
77
  + "["
75
- + validation_record["addressUsed"]
78
+ + address_used
76
79
  + ":"
77
- + validation_record["port"]
80
+ + port
78
81
  + "] Connect Timeout (Maybe firewall reasons)"
79
82
  )
80
83
  elif err_detail.endswith("Connection refused"):
81
84
  error["connect"] = {"error": "connection refused"}
85
+ address_used = validation_record.get("addressUsed", "unknown")
86
+ port = str(validation_record.get("port", "unknown"))
87
+ hostname = error.get("hostname", "unknown")
82
88
  message = (
83
- error["hostname"]
89
+ str(hostname)
84
90
  + "["
85
- + validation_record["addressUsed"]
91
+ + address_used
86
92
  + ":"
87
- + validation_record["port"]
93
+ + port
88
94
  + "] Connection Refused (Is http server running?)"
89
95
  )
90
96
  elif err_detail:
@@ -98,11 +104,13 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
98
104
 
99
105
  if match:
100
106
  error["response"] = (match.group(1) if match is not None else err_detail,)
101
- error["status_code"] = (error["status"],)
107
+ error["status_code"] = (error.get("status"),)
108
+ hostname = error.get("hostname", "unknown")
109
+ status = error.get("status", "unknown")
102
110
  message = (
103
- error["hostname"]
111
+ str(hostname)
104
112
  + " Status="
105
- + error["status"]
113
+ + str(status)
106
114
  + ": Invalid response in challenge url"
107
115
  )
108
116
  else:
@@ -131,7 +139,8 @@ class AcmeInvaliOrderError(AcmeHttpError):
131
139
  super().__init__(response, step)
132
140
 
133
141
 
134
- class AcmeInvaliNonceError(AcmeHttpError):
142
+ class AcmeInvalidNonceError(AcmeHttpError):
135
143
  def __init__(self, response: requests.Response, step: str):
136
144
  super().__init__(response, step)
137
145
  self.can_retry = True
146
+ self.response=0
@@ -34,7 +34,7 @@ def request(method, step: str, url: str, json=None, headers=None, throw=True) ->
34
34
  if json_data and json_data.get("type"):
35
35
  errorType = json_data["type"]
36
36
  if errorType == "urn:ietf:params:acme:error:badNonce":
37
- raise AcmeInvaliNonceError(res, step=step)
37
+ raise AcmeInvalidNonceError(res, step=step)
38
38
 
39
39
  if throw:
40
40
  raise AcmeHttpError(res, step=step)
@@ -12,6 +12,7 @@ class CertApiException(Exception):
12
12
  self.detail = detail if detail is not None else {}
13
13
  self.step = step
14
14
  self.can_retry = False
15
+ self.retry_delay = 4 # Default retry delay in seconds
15
16
 
16
17
  def json_obj(self) -> dict:
17
18
  return {"name": self.__class__.__name__, "message": self.message, "step": self.step, "detail": self.detail}
@@ -24,7 +24,7 @@ class HttpClientBase(object):
24
24
  try:
25
25
  res = self.session.request(method, url, json=json_data, data=data, params=params, timeout=timeout)
26
26
  except requests.exceptions.ConnectionError as e:
27
- print("Request [" + str(res.status_code) + "] : " + method + " " + url + " step=" + str(step))
27
+ print("Request [Connection Error] : " + method + " " + url + " step=" + str(step))
28
28
  raise NetworkError(
29
29
  request=e.request,
30
30
  message=f"Network connection error: {e}",
@@ -32,7 +32,7 @@ class HttpClientBase(object):
32
32
  step=f"HTTP Request ({method} {url})" if step is None else step,
33
33
  ) from e
34
34
  except requests.exceptions.Timeout as e:
35
- print("Request [" + str(res.status_code) + "] : " + method + " " + url + " step=" + str(step))
35
+ print("Request [Timeout] : " + method + " " + url + " step=" + str(step))
36
36
  raise NetworkError(
37
37
  request=e.request,
38
38
  message=f"Request timed out: {e}",
@@ -40,7 +40,7 @@ class HttpClientBase(object):
40
40
  step=f"HTTP Request ({method} {url})" if step is None else step,
41
41
  ) from e
42
42
  except requests.exceptions.RequestException as e:
43
- print("Request [" + str(res.status_code) + "] : " + method + " " + url + " step=" + str(step))
43
+ print("Request [Request Exception] : " + method + " " + url + " step=" + str(step))
44
44
  raise NetworkError(
45
45
  request=e.request,
46
46
  message=f"An unexpected network error occurred: {e}",
@@ -80,8 +80,9 @@ class HttpClientBase(object):
80
80
  return self._req(method, url, step, json_data, data, params, timeout)
81
81
  except CertApiException as e:
82
82
  if e.can_retry and i < retries:
83
- print(f"Retrying {method} {url} in {delay} seconds due to retryable error: {e.message}")
84
- time.sleep(delay)
83
+ retry_delay = e.retry_delay if hasattr(e, 'retry_delay') else delay
84
+ print(f"Retrying {method} {url} in {retry_delay} seconds due to retryable error: {e.message}")
85
+ time.sleep(retry_delay)
85
86
  else:
86
87
  e.can_retry = False
87
88
  raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: certapi
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Python Package for managing keys, request SSL certificates from ACME.
5
5
  Home-page: https://github.com/mesudip/certapi
6
6
  Author: Sudip Bhattarai
@@ -54,4 +54,5 @@ src/certapi/server/cert_api.py
54
54
  src/certapi/server/key_api.py
55
55
  tests/test_cert_issuer_generic.py
56
56
  tests/test_certs_with_key_types.py
57
+ tests/test_http_error_handling.py
57
58
  tests/test_keystores.py
@@ -1,5 +1,5 @@
1
1
  import pytest
2
- from datetime import datetime, timedelta, timezone
2
+ from datetime import UTC, datetime, timedelta, timezone
3
3
  from cryptography import x509
4
4
  from cryptography.x509.oid import NameOID, ExtensionOID
5
5
  from certapi import Key, SelfCertIssuer, Certificate, CertificateSigningRequest, CertificateSigningRequestBuilder
@@ -58,10 +58,10 @@ def test_generate_key_and_cert(self_cert_issuer_instance: SelfCertIssuer, key_ty
58
58
  assert set(san_values) == set(unique_expected_sans)
59
59
 
60
60
  # Verify expiry
61
- now_utc = datetime.now(timezone.utc)
61
+ now_utc = datetime.now(UTC)
62
62
  # Ensure certificate validity dates are timezone-aware UTC for comparison
63
- cert_not_valid_before_utc = cert.not_valid_before.astimezone(timezone.utc)
64
- cert_not_valid_after_utc = cert.not_valid_after.astimezone(timezone.utc)
63
+ cert_not_valid_before_utc = cert.not_valid_before_utc.astimezone(timezone.utc)
64
+ cert_not_valid_after_utc = cert.not_valid_after_utc.astimezone(timezone.utc)
65
65
 
66
66
  assert cert_not_valid_before_utc <= now_utc
67
67
  assert cert_not_valid_after_utc >= now_utc + timedelta(days=expiry_days - 1) # Allow for slight time difference
@@ -0,0 +1,140 @@
1
+ import pytest
2
+ from unittest.mock import Mock, patch
3
+ import requests
4
+ from certapi.http.HttpClientBase import HttpClientBase
5
+ from certapi.errors import NetworkError, CertApiException
6
+
7
+
8
+ class TestHttpErrorHandling:
9
+ """Test suite for HTTP error handling in HttpClientBase"""
10
+
11
+ @pytest.fixture
12
+ def http_client(self):
13
+ """Create a basic HttpClientBase instance for testing"""
14
+ return HttpClientBase(
15
+ base_url="https://example.com",
16
+ headers={"Content-Type": "application/json"},
17
+ auto_retry=True
18
+ )
19
+
20
+ def test_connection_reset_error_handling(self, http_client):
21
+ """Test that ConnectionResetError is properly caught and converted to NetworkError"""
22
+ with patch.object(http_client.session, 'request') as mock_request:
23
+ # Simulate a ConnectionResetError wrapped in requests.exceptions.ConnectionError
24
+ connection_reset = ConnectionResetError(104, 'Connection reset by peer')
25
+ mock_request.side_effect = requests.exceptions.ConnectionError(
26
+ connection_reset
27
+ )
28
+
29
+ with pytest.raises(NetworkError) as exc_info:
30
+ http_client._req("GET", "https://example.com/test", "Test Step")
31
+
32
+ # Verify the NetworkError is properly created
33
+ error = exc_info.value
34
+ assert error.can_retry is True
35
+ assert "Network connection error" in error.message
36
+ assert error.detail["errorType"] == "ConnectionError"
37
+
38
+ def test_connection_error_is_retriable(self, http_client):
39
+ """Test that connection errors are marked as retriable"""
40
+ with patch.object(http_client.session, 'request') as mock_request:
41
+ mock_request.side_effect = requests.exceptions.ConnectionError("Connection failed")
42
+
43
+ with pytest.raises(NetworkError) as exc_info:
44
+ http_client._req("GET", "https://example.com/test", "Test Step")
45
+
46
+ error = exc_info.value
47
+ assert error.can_retry is True
48
+ assert hasattr(error, 'retry_delay')
49
+ assert error.retry_delay == 4 # Default retry delay
50
+
51
+ def test_timeout_error_is_retriable(self, http_client):
52
+ """Test that timeout errors are marked as retriable"""
53
+ with patch.object(http_client.session, 'request') as mock_request:
54
+ mock_request.side_effect = requests.exceptions.Timeout("Request timed out")
55
+
56
+ with pytest.raises(NetworkError) as exc_info:
57
+ http_client._req("GET", "https://example.com/test", "Test Step")
58
+
59
+ error = exc_info.value
60
+ assert error.can_retry is True
61
+ assert hasattr(error, 'retry_delay')
62
+
63
+ def test_retry_uses_exception_delay(self, http_client):
64
+ """Test that retry mechanism uses the delay from the exception"""
65
+ call_count = 0
66
+
67
+ def side_effect(*args, **kwargs):
68
+ nonlocal call_count
69
+ call_count += 1
70
+ if call_count < 3: # Fail first 2 times
71
+ raise requests.exceptions.ConnectionError("Connection failed")
72
+ # Success on 3rd attempt
73
+ mock_response = Mock()
74
+ mock_response.status_code = 200
75
+ return mock_response
76
+
77
+ with patch.object(http_client.session, 'request', side_effect=side_effect):
78
+ with patch('time.sleep') as mock_sleep:
79
+ result = http_client._req_with_retry(
80
+ "GET", "https://example.com/test", "Test Step", retries=2
81
+ )
82
+
83
+ assert result.status_code == 200
84
+ # Should have slept twice (after 1st and 2nd failures)
85
+ assert mock_sleep.call_count == 2
86
+ # Should use default delay of 4 seconds
87
+ mock_sleep.assert_called_with(4)
88
+
89
+ def test_retry_exhausted_marks_non_retriable(self, http_client):
90
+ """Test that after exhausting retries, error is marked as non-retriable"""
91
+ with patch.object(http_client.session, 'request') as mock_request:
92
+ mock_request.side_effect = requests.exceptions.ConnectionError("Connection failed")
93
+
94
+ with pytest.raises(NetworkError) as exc_info:
95
+ http_client._req_with_retry(
96
+ "GET", "https://example.com/test", "Test Step", retries=1
97
+ )
98
+
99
+ error = exc_info.value
100
+ # After retries are exhausted, can_retry should be False
101
+ assert error.can_retry is False
102
+
103
+ def test_no_retry_when_auto_retry_false(self, http_client):
104
+ """Test that retry doesn't happen when auto_retry is False"""
105
+ http_client.auto_retry = False
106
+ call_count = 0
107
+
108
+ def side_effect(*args, **kwargs):
109
+ nonlocal call_count
110
+ call_count += 1
111
+ raise requests.exceptions.ConnectionError("Connection failed")
112
+
113
+ with patch.object(http_client.session, 'request', side_effect=side_effect):
114
+ with pytest.raises(NetworkError):
115
+ http_client._req_with_retry(
116
+ "GET", "https://example.com/test", "Test Step", retries=2
117
+ )
118
+
119
+ # Should only be called once (no retries)
120
+ assert call_count == 1
121
+
122
+ def test_successful_request_no_error(self, http_client):
123
+ """Test that successful requests don't raise errors"""
124
+ with patch.object(http_client.session, 'request') as mock_request:
125
+ mock_response = Mock()
126
+ mock_response.status_code = 200
127
+ mock_request.return_value = mock_response
128
+
129
+ result = http_client._req("GET", "https://example.com/test", "Test Step")
130
+
131
+ assert result.status_code == 200
132
+
133
+ def test_custom_retry_delay(self):
134
+ """Test that custom retry delay can be set on exception"""
135
+ exception = CertApiException("Test error")
136
+ exception.retry_delay = 10
137
+ exception.can_retry = True
138
+
139
+ assert exception.retry_delay == 10
140
+ assert exception.can_retry is True
@@ -1,3 +1,4 @@
1
+ from time import timezone
1
2
  import pytest
2
3
  import os
3
4
  import psycopg2 # Added for PostgreSQL database creation
@@ -7,7 +8,7 @@ from certapi import Key, Certificate
7
8
  from certapi.crypto.crypto import cert_to_pem, certs_to_pem
8
9
  from certapi.keystore import SqliteKeyStore, FileSystemKeyStore, PostgresKeyStore
9
10
  from typing import List, Tuple, Union
10
- from datetime import datetime, timedelta
11
+ from datetime import UTC, datetime, timedelta
11
12
  from certapi import KeyStore, Certificate, Key
12
13
 
13
14
  from cryptography import x509
@@ -163,14 +164,15 @@ def test_get_cert_by_id(keystore: KeyStore, ca_key: Key):
163
164
 
164
165
 
165
166
  def sign_csr(csr: x509.CertificateSigningRequest, issuer_key: Key, days_valid=365) -> Certificate:
167
+ now = datetime.now(UTC)
166
168
  builder = (
167
169
  x509.CertificateBuilder()
168
170
  .subject_name(csr.subject)
169
171
  .issuer_name(x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, "certapi.pytest.com")]))
170
172
  .public_key(csr.public_key())
171
173
  .serial_number(x509.random_serial_number())
172
- .not_valid_before(datetime.utcnow())
173
- .not_valid_after(datetime.utcnow() + timedelta(days=days_valid))
174
+ .not_valid_before(now)
175
+ .not_valid_after(now+ timedelta(days=days_valid))
174
176
  )
175
177
 
176
178
  # Optionally copy extensions from CSR
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes