certapi 1.1.3__tar.gz → 1.1.4__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 (70) hide show
  1. {certapi-1.1.3/src/certapi.egg-info → certapi-1.1.4}/PKG-INFO +1 -1
  2. {certapi-1.1.3 → certapi-1.1.4}/setup.py +1 -1
  3. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/client/renewal_manager.py +37 -15
  4. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/manager/acme_cert_manager.py +14 -1
  5. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/server/api.py +17 -35
  6. {certapi-1.1.3 → certapi-1.1.4/src/certapi.egg-info}/PKG-INFO +1 -1
  7. {certapi-1.1.3 → certapi-1.1.4}/tests/test_acme_cert_manager_batching.py +40 -0
  8. {certapi-1.1.3 → certapi-1.1.4}/tests/test_obtain_interface.py +15 -5
  9. {certapi-1.1.3 → certapi-1.1.4}/tests/test_renewal_manager.py +34 -3
  10. {certapi-1.1.3 → certapi-1.1.4}/MANIFEST.in +0 -0
  11. {certapi-1.1.3 → certapi-1.1.4}/README.md +0 -0
  12. {certapi-1.1.3 → certapi-1.1.4}/pyproject.toml +0 -0
  13. {certapi-1.1.3 → certapi-1.1.4}/setup.cfg +0 -0
  14. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/__init__.py +0 -0
  15. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/Acme.py +0 -0
  16. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/AcmeError.py +0 -0
  17. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/Challenge.py +0 -0
  18. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/Order.py +0 -0
  19. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/__init__.py +0 -0
  20. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/acme/http.py +0 -0
  21. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
  22. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
  23. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +0 -0
  24. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/__init__.py +0 -0
  25. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/__init__.py +0 -0
  26. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
  27. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
  28. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
  29. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
  30. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
  31. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
  32. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/cli.py +0 -0
  33. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/client/__init__.py +0 -0
  34. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/client/cert_manager_client.py +0 -0
  35. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/crypto/__init__.py +0 -0
  36. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/crypto/crypto.py +0 -0
  37. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/crypto/crypto_classes.py +0 -0
  38. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/domain_batching.py +0 -0
  39. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/errors.py +0 -0
  40. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/http/HttpClientBase.py +0 -0
  41. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/http/__init__.py +0 -0
  42. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/http/types.py +0 -0
  43. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/issuers/AcmeCertIssuer.py +0 -0
  44. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/issuers/SelfCertIssuer.py +0 -0
  45. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/issuers/__init__.py +0 -0
  46. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/issuers/abstract_certissuer.py +0 -0
  47. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
  48. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/KeyStore.py +0 -0
  49. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
  50. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/RemoteKeyStore.py +0 -0
  51. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/SqliteKeyStore.py +0 -0
  52. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/keystore/__init__.py +0 -0
  53. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/manager/__init__.py +0 -0
  54. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/server/__init__.py +0 -0
  55. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/server/cert_api.py +0 -0
  56. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/server/key_api.py +0 -0
  57. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/util.py +0 -0
  58. {certapi-1.1.3 → certapi-1.1.4}/src/certapi/utils.py +0 -0
  59. {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/SOURCES.txt +0 -0
  60. {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/dependency_links.txt +0 -0
  61. {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/entry_points.txt +0 -0
  62. {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/requires.txt +0 -0
  63. {certapi-1.1.3 → certapi-1.1.4}/src/certapi.egg-info/top_level.txt +0 -0
  64. {certapi-1.1.3 → certapi-1.1.4}/tests/test_acme_error_handling.py +0 -0
  65. {certapi-1.1.3 → certapi-1.1.4}/tests/test_cert_issuer_generic.py +0 -0
  66. {certapi-1.1.3 → certapi-1.1.4}/tests/test_certs_with_key_types.py +0 -0
  67. {certapi-1.1.3 → certapi-1.1.4}/tests/test_cli.py +0 -0
  68. {certapi-1.1.3 → certapi-1.1.4}/tests/test_domain_batching_boulder_cases.py +0 -0
  69. {certapi-1.1.3 → certapi-1.1.4}/tests/test_http_error_handling.py +0 -0
  70. {certapi-1.1.3 → certapi-1.1.4}/tests/test_keystores.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: certapi
3
- Version: 1.1.3
3
+ Version: 1.1.4
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
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="certapi",
5
- version="1.1.3",
5
+ version="1.1.4",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -2,7 +2,7 @@ import os
2
2
  import threading
3
3
  import time
4
4
  from datetime import datetime, timezone, timedelta
5
- from typing import Callable, Dict, Optional, Set, Any, List
5
+ from typing import Callable, Dict, Optional, Set, Any, List, Literal
6
6
 
7
7
  from certapi.crypto import Key, certs_from_pem
8
8
  from certapi.client.cert_manager_client import CertManagerClient
@@ -42,9 +42,27 @@ class RenewalManager:
42
42
  remote_poll_interval_seconds: int = 30,
43
43
  clock_fn: Optional[Callable[[], datetime]] = None,
44
44
  sleep_fn: Optional[Callable[[float], None]] = None,
45
+ key_type: Literal["rsa", "ecdsa", "ed25519"] = "ecdsa",
46
+ expiry_days: int = 90,
47
+ batch_domains: bool = False,
48
+ self_verify: bool = True,
49
+ country: Optional[str] = None,
50
+ state: Optional[str] = None,
51
+ locality: Optional[str] = None,
52
+ organization: Optional[str] = None,
53
+ user_id: Optional[str] = None,
45
54
  ):
46
55
  self.cert_manager_client = cert_manager_client
47
56
  self.sync_watch_domains = sync_watch_domains
57
+ self.key_type = key_type
58
+ self.expiry_days = expiry_days
59
+ self.batch_domains = batch_domains
60
+ self.self_verify = self_verify
61
+ self.country = country
62
+ self.state = state
63
+ self.locality = locality
64
+ self.organization = organization
65
+ self.user_id = user_id
48
66
  self.sleep_slack_seconds = sleep_slack_seconds
49
67
  self.max_sleep_seconds = max_sleep_seconds
50
68
  self.renew_retry_interval_seconds = renew_retry_interval_seconds
@@ -293,20 +311,24 @@ class RenewalManager:
293
311
 
294
312
  try:
295
313
  renew_threshold_days = self.cert_min_renew_threshold_secs // (24 * 3600)
296
- obtain_fn = getattr(self.cert_manager_client, "obtain", None)
297
- if callable(obtain_fn):
298
- res = self._call_certificate_backend(
299
- obtain_fn,
300
- [domain],
301
- {"renew_threshold_days": renew_threshold_days, "skip_failing": False},
302
- )
303
- else:
304
- # Backward compatibility for clients that only implement issue_certificate.
305
- res = self._call_certificate_backend(
306
- self.cert_manager_client.issue_certificate,
307
- [domain],
308
- {"renew_threshold_days": renew_threshold_days},
309
- )
314
+ obtain_options = {
315
+ "key_type": self.key_type,
316
+ "expiry_days": self.expiry_days,
317
+ "renew_threshold_days": renew_threshold_days,
318
+ "skip_failing": False,
319
+ "batch_domains": self.batch_domains,
320
+ "self_verify": self.self_verify,
321
+ "country": self.country,
322
+ "state": self.state,
323
+ "locality": self.locality,
324
+ "organization": self.organization,
325
+ "user_id": self.user_id,
326
+ }
327
+ res = self._call_certificate_backend(
328
+ self.cert_manager_client.obtain,
329
+ [domain],
330
+ obtain_options,
331
+ )
310
332
  if res is None:
311
333
  return
312
334
  self._update_expiry_cache(res.issued + res.existing)
@@ -75,6 +75,8 @@ class AcmeCertManager:
75
75
  organization: Optional[str] = None,
76
76
  user_id: Optional[str] = None,
77
77
  renew_threshold_days: Optional[int] = None,
78
+ skip_failing: bool = True,
79
+ batch_domains: bool = False,
78
80
  self_verify: bool = True,
79
81
  ) -> CertificateResponse:
80
82
  return self.obtain(
@@ -87,6 +89,8 @@ class AcmeCertManager:
87
89
  organization=organization,
88
90
  user_id=user_id,
89
91
  renew_threshold_days=renew_threshold_days,
92
+ skip_failing=skip_failing,
93
+ batch_domains=batch_domains,
90
94
  self_verify=self_verify,
91
95
  )
92
96
 
@@ -101,6 +105,8 @@ class AcmeCertManager:
101
105
  organization: Optional[str] = None,
102
106
  user_id: Optional[str] = None,
103
107
  renew_threshold_days: Optional[int] = None,
108
+ skip_failing: bool = True,
109
+ batch_domains: bool = False,
104
110
  self_verify: bool = True,
105
111
  ) -> CertificateResponse:
106
112
  return self._issue_certificate_internal(
@@ -113,7 +119,8 @@ class AcmeCertManager:
113
119
  organization=organization,
114
120
  user_id=user_id,
115
121
  renew_threshold_days=renew_threshold_days,
116
- batch_generator=None,
122
+ batch_generator=create_safe_domain_batches if batch_domains else None,
123
+ skip_failing=skip_failing,
117
124
  self_verify=self_verify,
118
125
  )
119
126
 
@@ -129,6 +136,7 @@ class AcmeCertManager:
129
136
  user_id: Optional[str] = None,
130
137
  renew_threshold_days: Optional[int] = None,
131
138
  batch_generator: Callable[[List[str]], List[List[str]]] = create_safe_domain_batches,
139
+ skip_failing: bool = True,
132
140
  self_verify: bool = True,
133
141
  ) -> CertificateResponse:
134
142
  return self._issue_certificate_internal(
@@ -142,6 +150,7 @@ class AcmeCertManager:
142
150
  user_id=user_id,
143
151
  renew_threshold_days=renew_threshold_days,
144
152
  batch_generator=batch_generator,
153
+ skip_failing=skip_failing,
145
154
  self_verify=self_verify,
146
155
  )
147
156
 
@@ -157,6 +166,7 @@ class AcmeCertManager:
157
166
  user_id: Optional[str] = None,
158
167
  renew_threshold_days: Optional[int] = None,
159
168
  batch_generator: Optional[Callable[[List[str]], List[List[str]]]] = None,
169
+ skip_failing: bool = True,
160
170
  self_verify: bool = True,
161
171
  ) -> CertificateResponse:
162
172
  if isinstance(hosts, str):
@@ -196,6 +206,9 @@ class AcmeCertManager:
196
206
  else:
197
207
  print(f"Warning: No challenge solver found that supports domain: {host}. Skipping.")
198
208
 
209
+ if len(domains_by_store) == 0 and not skip_failing:
210
+ raise ValueError("None of the domains are owned by this machine or could be verified")
211
+
199
212
  for store, domains_to_issue in domains_by_store.items():
200
213
  issuance_batches = (
201
214
  batch_generator(domains_to_issue) if batch_generator is not None else [domains_to_issue]
@@ -122,43 +122,25 @@ def create_api_resources(api_ns, cert_manager: AcmeCertManager, renew_queue_size
122
122
  def get(self):
123
123
  args = obtain_parser.parse_args()
124
124
  hostnames = args["hostname"]
125
- skip_failing = args.get("skip_failing", False)
126
- self_verify = args.get("self_verify", True)
127
125
 
128
126
  with lock_manager:
129
- if self_verify:
130
- verified_hostnames = []
131
- for h in hostnames:
132
- # Find the first solver that supports and can verify this domain.
133
- for solver in reversed(cert_manager.challenge_solvers):
134
- if solver.supports_domain_strict(h):
135
- verified_hostnames.append(h)
136
-
137
- hostnames = verified_hostnames
138
-
139
- if not hostnames and not skip_failing:
140
- api_ns.abort(400, message="None of the domains are owned by this machine or could be verified")
141
-
142
- if not hostnames:
143
- return CertificateResponse().to_json()
144
-
145
- issue_fn = (
146
- cert_manager.issue_certificate_in_batches
147
- if args.get("batch_domains")
148
- else cert_manager.issue_certificate
149
- )
150
- data = issue_fn(
151
- hostnames,
152
- key_type=args["key_type"],
153
- expiry_days=args["expiry_days"],
154
- country=args["country"],
155
- state=args["state"],
156
- locality=args["locality"],
157
- organization=args["organization"],
158
- user_id=args["user_id"],
159
- renew_threshold_days=args.get("renew_threshold_days"),
160
- self_verify=False,
161
- )
127
+ try:
128
+ data = cert_manager.obtain(
129
+ hostnames,
130
+ key_type=args["key_type"],
131
+ expiry_days=args["expiry_days"],
132
+ country=args["country"],
133
+ state=args["state"],
134
+ locality=args["locality"],
135
+ organization=args["organization"],
136
+ user_id=args["user_id"],
137
+ renew_threshold_days=args.get("renew_threshold_days"),
138
+ skip_failing=args.get("skip_failing", False),
139
+ batch_domains=args.get("batch_domains", False),
140
+ self_verify=args.get("self_verify", True),
141
+ )
142
+ except ValueError as e:
143
+ api_ns.abort(400, message=str(e))
162
144
 
163
145
  print(data)
164
146
  if data:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: certapi
3
- Version: 1.1.3
3
+ Version: 1.1.4
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
@@ -98,6 +98,26 @@ def test_issue_certificate_in_batches_uses_custom_batch_generator():
98
98
  assert [issued.domains for issued in response.issued] == issuer.calls
99
99
 
100
100
 
101
+ def test_obtain_batch_domains_uses_safe_batches():
102
+ key_store = DummyKeyStore()
103
+ issuer = DummyIssuer()
104
+ solver = DummySolver()
105
+ manager = AcmeCertManager(key_store=key_store, cert_issuer=issuer, challenge_solvers=[solver])
106
+
107
+ domains = [
108
+ "x.abc.def.ghi.example.com",
109
+ "root.example.com",
110
+ ]
111
+ response = manager.obtain(hosts=domains, batch_domains=True)
112
+
113
+ assert issuer.calls == [
114
+ ["root.example.com"],
115
+ ["x.abc.def"],
116
+ ["ghi.example.com"],
117
+ ]
118
+ assert len(response.issued) == 3
119
+
120
+
101
121
  def test_issue_certificate_self_verify_false_skips_strict_solver_check():
102
122
  class StrictFailingSolver(DummySolver):
103
123
  def supports_domain_strict(self, _domain):
@@ -127,3 +147,23 @@ def test_issue_certificate_self_verify_true_uses_strict_solver_check():
127
147
 
128
148
  assert issuer.calls == []
129
149
  assert response.issued == []
150
+
151
+
152
+ def test_obtain_self_verify_true_raises_when_all_domains_fail_and_skip_failing_false():
153
+ class StrictFailingSolver(DummySolver):
154
+ def supports_domain_strict(self, _domain):
155
+ return False
156
+
157
+ key_store = DummyKeyStore()
158
+ issuer = DummyIssuer()
159
+ solver = StrictFailingSolver()
160
+ manager = AcmeCertManager(key_store=key_store, cert_issuer=issuer, challenge_solvers=[solver])
161
+
162
+ try:
163
+ manager.obtain(hosts=["force.example.com"], skip_failing=False)
164
+ except ValueError as e:
165
+ assert "None of the domains are owned" in str(e)
166
+ else:
167
+ raise AssertionError("expected strict obtain failure")
168
+
169
+ assert issuer.calls == []
@@ -4,6 +4,7 @@ from flask import Flask
4
4
  from flask_restx import Api, Namespace
5
5
 
6
6
  from certapi.client.cert_manager_client import CertManagerClient
7
+ from certapi.domain_batching import create_safe_domain_batches
7
8
  from certapi.http.types import CertificateResponse
8
9
  from certapi.manager.acme_cert_manager import AcmeCertManager
9
10
  from certapi.server.api import create_api_resources
@@ -29,6 +30,8 @@ def test_acme_cert_manager_issue_certificate_delegates_to_obtain(monkeypatch):
29
30
  organization="Org",
30
31
  user_id="u1",
31
32
  renew_threshold_days=25,
33
+ skip_failing=False,
34
+ batch_domains=True,
32
35
  self_verify=False,
33
36
  )
34
37
 
@@ -42,6 +45,8 @@ def test_acme_cert_manager_issue_certificate_delegates_to_obtain(monkeypatch):
42
45
  assert captured["organization"] == "Org"
43
46
  assert captured["user_id"] == "u1"
44
47
  assert captured["renew_threshold_days"] == 25
48
+ assert captured["skip_failing"] is False
49
+ assert captured["batch_domains"] is True
45
50
  assert captured["self_verify"] is False
46
51
 
47
52
 
@@ -60,6 +65,8 @@ def test_acme_cert_manager_obtain_uses_internal_issue_path(monkeypatch):
60
65
  key_type="ecdsa",
61
66
  expiry_days=90,
62
67
  renew_threshold_days=30,
68
+ skip_failing=False,
69
+ batch_domains=True,
63
70
  self_verify=False,
64
71
  )
65
72
 
@@ -68,7 +75,8 @@ def test_acme_cert_manager_obtain_uses_internal_issue_path(monkeypatch):
68
75
  assert captured["key_type"] == "ecdsa"
69
76
  assert captured["expiry_days"] == 90
70
77
  assert captured["renew_threshold_days"] == 30
71
- assert captured["batch_generator"] is None
78
+ assert captured["batch_generator"] is create_safe_domain_batches
79
+ assert captured["skip_failing"] is False
72
80
  assert captured["self_verify"] is False
73
81
 
74
82
 
@@ -157,7 +165,7 @@ def test_remote_obtain_self_verify_false_skips_server_prefilter():
157
165
  def __init__(self):
158
166
  self.calls = []
159
167
 
160
- def issue_certificate(self, hosts, **kwargs):
168
+ def obtain(self, hosts, **kwargs):
161
169
  self.calls.append({"hosts": hosts, "kwargs": kwargs})
162
170
  return CertificateResponse()
163
171
 
@@ -171,7 +179,7 @@ def test_remote_obtain_self_verify_false_skips_server_prefilter():
171
179
  assert manager.calls[0]["kwargs"]["self_verify"] is False
172
180
 
173
181
 
174
- def test_remote_obtain_self_verify_true_keeps_server_prefilter():
182
+ def test_remote_obtain_self_verify_true_delegates_strict_failure_to_manager():
175
183
  app = Flask(__name__)
176
184
  api = Api(app)
177
185
  api_ns = Namespace("api")
@@ -184,8 +192,10 @@ def test_remote_obtain_self_verify_true_keeps_server_prefilter():
184
192
  class Manager:
185
193
  challenge_solvers = [Solver()]
186
194
 
187
- def issue_certificate(self, hosts, **kwargs):
188
- raise AssertionError("unverified host should not reach manager")
195
+ def obtain(self, hosts, **kwargs):
196
+ assert hosts == ["force.example.com"]
197
+ assert kwargs["self_verify"] is True
198
+ raise ValueError("None of the domains are owned by this machine or could be verified")
189
199
 
190
200
  create_api_resources(api_ns, Manager())
191
201
 
@@ -23,7 +23,7 @@ class DummyClient:
23
23
  def set_handler(self, domain, handler):
24
24
  self._handlers[domain] = handler
25
25
 
26
- def issue_certificate(self, hosts, **kwargs):
26
+ def obtain(self, hosts, **kwargs):
27
27
  host = hosts[0] if isinstance(hosts, list) else hosts
28
28
  self.calls.append({"host": host, "kwargs": kwargs})
29
29
  handler = self._handlers.get(host)
@@ -35,6 +35,9 @@ class DummyClient:
35
35
  return handler(host, kwargs)
36
36
  return handler
37
37
 
38
+ def issue_certificate(self, hosts, **kwargs):
39
+ return self.obtain(hosts, **kwargs)
40
+
38
41
 
39
42
  class DummyClientWithObtain(DummyClient):
40
43
  def __init__(self):
@@ -45,12 +48,12 @@ class DummyClientWithObtain(DummyClient):
45
48
  def obtain(self, hosts, **kwargs):
46
49
  host = hosts[0] if isinstance(hosts, list) else hosts
47
50
  self.obtain_calls.append({"host": host, "kwargs": kwargs})
48
- return super().issue_certificate(hosts, **kwargs)
51
+ return super().obtain(hosts, **kwargs)
49
52
 
50
53
  def issue_certificate(self, hosts, **kwargs):
51
54
  host = hosts[0] if isinstance(hosts, list) else hosts
52
55
  self.issue_calls.append({"host": host, "kwargs": kwargs})
53
- return super().issue_certificate(hosts, **kwargs)
56
+ return super().obtain(hosts, **kwargs)
54
57
 
55
58
 
56
59
  class DummyRemoteClient(CertManagerClient):
@@ -408,6 +411,34 @@ def test_renewal_manager_prefers_obtain_when_available():
408
411
  assert len(client.issue_calls) == 0
409
412
 
410
413
 
414
+ def test_renewal_manager_passes_typed_obtain_options_into_renewal_calls():
415
+ now = datetime(2026, 1, 1, tzinfo=UTC)
416
+ client = DummyClient()
417
+ cert = _make_cert_pem("batch.example.com", now, valid_for_days=60)
418
+ client.set_handler(
419
+ "batch.example.com",
420
+ CertificateResponse(issued=[IssuedCert(cert=cert, domains=["batch.example.com"])], existing=[]),
421
+ )
422
+
423
+ mgr = RenewalManager(
424
+ client,
425
+ renew_threshold_days=30,
426
+ clock_fn=lambda: now,
427
+ batch_domains=True,
428
+ self_verify=False,
429
+ organization="certapi-tests",
430
+ )
431
+ mgr.set_watch_domains(["batch.example.com"])
432
+ mgr.trigger_now()
433
+
434
+ assert len(client.calls) == 1
435
+ assert client.calls[0]["kwargs"]["batch_domains"] is True
436
+ assert client.calls[0]["kwargs"]["self_verify"] is False
437
+ assert client.calls[0]["kwargs"]["skip_failing"] is False
438
+ assert client.calls[0]["kwargs"]["renew_threshold_days"] == 30
439
+ assert client.calls[0]["kwargs"]["organization"] == "certapi-tests"
440
+
441
+
411
442
  def test_local_keystore_seed_prevents_remote_call_when_fresh():
412
443
  now = datetime(2026, 1, 1, tzinfo=UTC)
413
444
  key = Key.generate("ecdsa")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes