certapi 1.1.7__tar.gz → 1.1.9__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.
- {certapi-1.1.7/src/certapi.egg-info → certapi-1.1.9}/PKG-INFO +1 -1
- {certapi-1.1.7 → certapi-1.1.9}/setup.py +1 -1
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/client/renewal_manager.py +20 -2
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/domain_batching.py +1 -15
- {certapi-1.1.7 → certapi-1.1.9/src/certapi.egg-info}/PKG-INFO +1 -1
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_acme_cert_manager_batching.py +5 -15
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_renewal_manager.py +23 -4
- {certapi-1.1.7 → certapi-1.1.9}/MANIFEST.in +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/README.md +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/pyproject.toml +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/setup.cfg +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/acme/Acme.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/acme/AcmeError.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/acme/Challenge.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/acme/Order.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/acme/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/acme/http.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/dns/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/cli.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/client/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/client/cert_manager_client.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/crypto/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/crypto/crypto.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/crypto/crypto_classes.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/errors.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/http/HttpClientBase.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/http/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/http/types.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/issuers/AcmeCertIssuer.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/issuers/SelfCertIssuer.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/issuers/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/issuers/abstract_certissuer.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/keystore/KeyStore.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/keystore/RemoteKeyStore.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/keystore/SqliteKeyStore.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/keystore/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/manager/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/manager/acme_cert_manager.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/server/__init__.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/server/api.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/server/cert_api.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/server/key_api.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/util.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi/utils.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi.egg-info/SOURCES.txt +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi.egg-info/entry_points.txt +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi.egg-info/requires.txt +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/src/certapi.egg-info/top_level.txt +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_acme_error_handling.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_cert_issuer_generic.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_certs_with_key_types.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_cli.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_domain_batching_boulder_cases.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_http_error_handling.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_keystores.py +0 -0
- {certapi-1.1.7 → certapi-1.1.9}/tests/test_obtain_interface.py +0 -0
|
@@ -476,7 +476,8 @@ class RenewalManager:
|
|
|
476
476
|
"key_type": self.key_type,
|
|
477
477
|
"expiry_days": self.expiry_days,
|
|
478
478
|
"renew_threshold_days": renew_threshold_days,
|
|
479
|
-
|
|
479
|
+
# Renewal should obtain whatever can be issued, then self-sign unresolved domains.
|
|
480
|
+
"skip_failing": True,
|
|
480
481
|
"batch_domains": True,
|
|
481
482
|
"self_verify": self.self_verify,
|
|
482
483
|
"country": self.country,
|
|
@@ -492,7 +493,16 @@ class RenewalManager:
|
|
|
492
493
|
)
|
|
493
494
|
if res is None:
|
|
494
495
|
return
|
|
495
|
-
|
|
496
|
+
certs = res.issued + res.existing
|
|
497
|
+
self._update_expiry_cache(certs)
|
|
498
|
+
covered_domains = self._covered_domains_from_response(certs)
|
|
499
|
+
unresolved_domains = [domain for domain in domains if domain not in covered_domains]
|
|
500
|
+
for domain in unresolved_domains:
|
|
501
|
+
self._add_to_blacklist(domain, now)
|
|
502
|
+
if domain in domains_with_cached_entries or self._has_local_certificate(domain):
|
|
503
|
+
self._schedule_existing_certificate_retry(domain, now)
|
|
504
|
+
else:
|
|
505
|
+
self._register_self_signed(domain)
|
|
496
506
|
except Exception as e:
|
|
497
507
|
self._set_error(e)
|
|
498
508
|
for domain in domains:
|
|
@@ -563,6 +573,14 @@ class RenewalManager:
|
|
|
563
573
|
self._cache = {d: exp for d, exp in self._cache.items() if d in self._watch_domains}
|
|
564
574
|
self._lock.notify_all()
|
|
565
575
|
|
|
576
|
+
def _covered_domains_from_response(self, certs) -> Set[str]:
|
|
577
|
+
covered: Set[str] = set()
|
|
578
|
+
for cert in certs:
|
|
579
|
+
for domain in cert.domains:
|
|
580
|
+
if domain:
|
|
581
|
+
covered.add(domain)
|
|
582
|
+
return covered
|
|
583
|
+
|
|
566
584
|
def _add_to_blacklist(self, domain: str, now: datetime):
|
|
567
585
|
with self._lock:
|
|
568
586
|
self._blacklist[domain] = now + timedelta(seconds=self.blacklist_duration_seconds)
|
|
@@ -36,19 +36,6 @@ def would_trigger(labels: list[str], blocked: set[str]) -> bool:
|
|
|
36
36
|
return False
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def split_domain_to_safe_groups(domain: str, blocked_labels: list[str] | None = None) -> list[list[str]]:
|
|
40
|
-
"""
|
|
41
|
-
Split labels into contiguous chunks of at most 3 labels.
|
|
42
|
-
This mirrors the "3 labels are always safe for this specific LE heuristic"
|
|
43
|
-
invariant, but these label groups are not used directly as issuance domains.
|
|
44
|
-
"""
|
|
45
|
-
labels = _split_labels(domain)
|
|
46
|
-
n = len(labels)
|
|
47
|
-
if n == 0:
|
|
48
|
-
return []
|
|
49
|
-
return [labels[i : i + 3] for i in range(0, n, 3)]
|
|
50
|
-
|
|
51
|
-
|
|
52
39
|
def create_safe_domain_batches(domains: list[str], blocked_labels: list[str] | None = None) -> list[list[str]]:
|
|
53
40
|
"""
|
|
54
41
|
Create compact issuance batches without any blocked-label list assumptions.
|
|
@@ -74,8 +61,7 @@ def create_safe_domain_batches(domains: list[str], blocked_labels: list[str] | N
|
|
|
74
61
|
compact_batch.append(normalized)
|
|
75
62
|
continue
|
|
76
63
|
|
|
77
|
-
|
|
78
|
-
singleton_batches.append([".".join(labels[i : i + 3])])
|
|
64
|
+
singleton_batches.append([normalized])
|
|
79
65
|
|
|
80
66
|
if compact_batch:
|
|
81
67
|
return [compact_batch, *singleton_batches]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from certapi.domain_batching import create_safe_domain_batches,
|
|
1
|
+
from certapi.domain_batching import create_safe_domain_batches, would_trigger
|
|
2
2
|
from certapi.manager.acme_cert_manager import AcmeCertManager
|
|
3
3
|
|
|
4
4
|
|
|
@@ -42,14 +42,6 @@ def test_would_trigger_exact_rule():
|
|
|
42
42
|
assert would_trigger(["x", "abc", "abc", "example", "com"], blocked) is True
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def test_split_domain_to_safe_groups_greedy():
|
|
46
|
-
groups = split_domain_to_safe_groups(
|
|
47
|
-
"x.abc.def.ghi.example.com",
|
|
48
|
-
blocked_labels=["abc", "def", "ghi"],
|
|
49
|
-
)
|
|
50
|
-
assert groups == [["x", "abc", "def"], ["ghi", "example", "com"]]
|
|
51
|
-
|
|
52
|
-
|
|
53
45
|
def test_create_safe_domain_batches_compacts_non_triggering_domains():
|
|
54
46
|
domains = ["x.example.com", "y.example.com", "y.example.com"]
|
|
55
47
|
assert create_safe_domain_batches(domains) == [["x.example.com", "y.example.com"]]
|
|
@@ -91,10 +83,9 @@ def test_issue_certificate_in_batches_uses_custom_batch_generator():
|
|
|
91
83
|
|
|
92
84
|
assert issuer.calls == [
|
|
93
85
|
["root.example.com"],
|
|
94
|
-
["x.abc.def"],
|
|
95
|
-
["ghi.example.com"],
|
|
86
|
+
["x.abc.def.ghi.example.com"],
|
|
96
87
|
]
|
|
97
|
-
assert len(response.issued) ==
|
|
88
|
+
assert len(response.issued) == 2
|
|
98
89
|
assert [issued.domains for issued in response.issued] == issuer.calls
|
|
99
90
|
|
|
100
91
|
|
|
@@ -112,10 +103,9 @@ def test_obtain_batch_domains_uses_safe_batches():
|
|
|
112
103
|
|
|
113
104
|
assert issuer.calls == [
|
|
114
105
|
["root.example.com"],
|
|
115
|
-
["x.abc.def"],
|
|
116
|
-
["ghi.example.com"],
|
|
106
|
+
["x.abc.def.ghi.example.com"],
|
|
117
107
|
]
|
|
118
|
-
assert len(response.issued) ==
|
|
108
|
+
assert len(response.issued) == 2
|
|
119
109
|
|
|
120
110
|
|
|
121
111
|
def test_issue_certificate_self_verify_false_skips_strict_solver_check():
|
|
@@ -425,7 +425,7 @@ def test_renewal_manager_passes_typed_obtain_options_into_renewal_calls():
|
|
|
425
425
|
assert len(client.calls) == 1
|
|
426
426
|
assert client.calls[0]["kwargs"]["batch_domains"] is True
|
|
427
427
|
assert client.calls[0]["kwargs"]["self_verify"] is False
|
|
428
|
-
assert client.calls[0]["kwargs"]["skip_failing"] is
|
|
428
|
+
assert client.calls[0]["kwargs"]["skip_failing"] is True
|
|
429
429
|
assert client.calls[0]["kwargs"]["renew_threshold_days"] == 30
|
|
430
430
|
assert client.calls[0]["kwargs"]["organization"] == "certapi-tests"
|
|
431
431
|
|
|
@@ -523,6 +523,25 @@ def test_new_domain_failure_selfsigns_and_blacklists():
|
|
|
523
523
|
assert len(client.calls) == 2
|
|
524
524
|
|
|
525
525
|
|
|
526
|
+
def test_partial_renewal_success_selfsigns_uncovered_domains():
|
|
527
|
+
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
528
|
+
client = DummyClient()
|
|
529
|
+
client.key_store = DummyKeyStore()
|
|
530
|
+
ok_cert = _make_cert_pem("ok.example.com", now, valid_for_days=60)
|
|
531
|
+
client.set_handler(
|
|
532
|
+
"ok.example.com",
|
|
533
|
+
CertificateResponse(issued=[IssuedCert(cert=ok_cert, domains=["ok.example.com"])], existing=[]),
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
mgr = RenewalManager(client, renew_threshold_days=30, clock_fn=lambda: now)
|
|
537
|
+
mgr._renew_domains(["ok.example.com", "missing.example.com"], now)
|
|
538
|
+
|
|
539
|
+
with mgr._lock:
|
|
540
|
+
assert "ok.example.com" in mgr._cache
|
|
541
|
+
assert len(client.key_store.saved_certs) == 1
|
|
542
|
+
assert client.key_store.saved_certs[0][2] == "missing.example.com.selfsigned"
|
|
543
|
+
|
|
544
|
+
|
|
526
545
|
def test_update_watch_domains_blocks_until_running_worker_processes_domains():
|
|
527
546
|
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
528
547
|
started = threading.Event()
|
|
@@ -672,7 +691,7 @@ def test_remote_certapi_polling_prints_waiting_message(capsys):
|
|
|
672
691
|
assert len(client.obtain_calls) == 1
|
|
673
692
|
|
|
674
693
|
|
|
675
|
-
def
|
|
694
|
+
def test_remote_renewal_uses_skip_failing_and_selfsigns_unresolved_domain():
|
|
676
695
|
now = datetime(2026, 1, 1, tzinfo=UTC)
|
|
677
696
|
client = DummyRemoteSkipFailingClient()
|
|
678
697
|
client.key_store = DummyKeyStore()
|
|
@@ -681,9 +700,9 @@ def test_remote_renewal_disables_skip_failing_so_unverified_domain_fails():
|
|
|
681
700
|
mgr.update_watch_domains(["missing.example.com"])
|
|
682
701
|
|
|
683
702
|
assert len(client.obtain_calls) == 1
|
|
684
|
-
assert client.obtain_calls[0]["kwargs"]["skip_failing"] is
|
|
703
|
+
assert client.obtain_calls[0]["kwargs"]["skip_failing"] is True
|
|
685
704
|
assert "missing.example.com" in mgr.get_state()["blacklisted_domains"]
|
|
686
|
-
assert
|
|
705
|
+
assert mgr.get_state()["last_error_message"] is None
|
|
687
706
|
assert len(client.key_store.saved_certs) == 1
|
|
688
707
|
assert client.key_store.saved_certs[0][2] == "missing.example.com.selfsigned"
|
|
689
708
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{certapi-1.1.7 → certapi-1.1.9}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|