certapi 0.6.0__tar.gz → 1.0.1__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 (63) hide show
  1. {certapi-0.6.0/src/certapi.egg-info → certapi-1.0.1}/PKG-INFO +47 -6
  2. certapi-1.0.1/README.md +93 -0
  3. {certapi-0.6.0 → certapi-1.0.1}/setup.py +1 -1
  4. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/__init__.py +1 -0
  5. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/AcmeError.py +5 -7
  6. certapi-1.0.1/src/certapi/client/__init__.py +1 -0
  7. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/http/HttpClientBase.py +1 -1
  8. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/issuers/AcmeCertIssuer.py +10 -0
  9. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/manager/acme_cert_manager.py +11 -3
  10. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/server/api.py +62 -11
  11. {certapi-0.6.0 → certapi-1.0.1/src/certapi.egg-info}/PKG-INFO +47 -6
  12. {certapi-0.6.0 → certapi-1.0.1}/src/certapi.egg-info/SOURCES.txt +1 -0
  13. certapi-1.0.1/tests/test_acme_error_handling.py +704 -0
  14. {certapi-0.6.0 → certapi-1.0.1}/tests/test_http_error_handling.py +33 -43
  15. {certapi-0.6.0 → certapi-1.0.1}/tests/test_keystores.py +1 -1
  16. certapi-0.6.0/README.md +0 -52
  17. certapi-0.6.0/src/certapi/server/__init__.py +0 -0
  18. {certapi-0.6.0 → certapi-1.0.1}/MANIFEST.in +0 -0
  19. {certapi-0.6.0 → certapi-1.0.1}/pyproject.toml +0 -0
  20. {certapi-0.6.0 → certapi-1.0.1}/setup.cfg +0 -0
  21. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/Acme.py +0 -0
  22. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/Challenge.py +0 -0
  23. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/Order.py +0 -0
  24. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/__init__.py +0 -0
  25. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/acme/http.py +0 -0
  26. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
  27. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
  28. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +0 -0
  29. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/__init__.py +0 -0
  30. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/__init__.py +0 -0
  31. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
  32. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
  33. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
  34. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
  35. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
  36. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
  37. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/client/cert_manager_client.py +0 -0
  38. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/crypto/__init__.py +0 -0
  39. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/crypto/crypto.py +0 -0
  40. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/crypto/crypto_classes.py +0 -0
  41. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/errors.py +0 -0
  42. {certapi-0.6.0/src/certapi/client → certapi-1.0.1/src/certapi/http}/__init__.py +0 -0
  43. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/http/types.py +0 -0
  44. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/issuers/SelfCertIssuer.py +0 -0
  45. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/issuers/__init__.py +0 -0
  46. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/issuers/abstract_certissuer.py +0 -0
  47. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
  48. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/KeyStore.py +0 -0
  49. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
  50. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/RemoteKeyStore.py +0 -0
  51. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/SqliteKeyStore.py +0 -0
  52. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/keystore/__init__.py +0 -0
  53. {certapi-0.6.0/src/certapi/http → certapi-1.0.1/src/certapi/manager}/__init__.py +0 -0
  54. {certapi-0.6.0/src/certapi/manager → certapi-1.0.1/src/certapi/server}/__init__.py +0 -0
  55. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/server/cert_api.py +0 -0
  56. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/server/key_api.py +0 -0
  57. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/util.py +0 -0
  58. {certapi-0.6.0 → certapi-1.0.1}/src/certapi/utils.py +0 -0
  59. {certapi-0.6.0 → certapi-1.0.1}/src/certapi.egg-info/dependency_links.txt +0 -0
  60. {certapi-0.6.0 → certapi-1.0.1}/src/certapi.egg-info/requires.txt +0 -0
  61. {certapi-0.6.0 → certapi-1.0.1}/src/certapi.egg-info/top_level.txt +0 -0
  62. {certapi-0.6.0 → certapi-1.0.1}/tests/test_cert_issuer_generic.py +0 -0
  63. {certapi-0.6.0 → certapi-1.0.1}/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.6.0
3
+ Version: 1.0.1
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
@@ -27,7 +27,10 @@ Dynamic: summary
27
27
  CertApi is a Python package for requesting SSL certificates from ACME.
28
28
  This is to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
29
29
 
30
- > ⚠️ Warning: This project is in beta. Please stay tuned for the LTS `v1.0.0` release.
30
+ [![Build Status](https://github.com/mesudip/certapi/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
31
+ [![codecov](https://codecov.io/github/mesudip/certapi/graph/badge.svg?token=NYTNCH29IT)](https://codecov.io/github/mesudip/certapi)
32
+ [![PyPI version](https://img.shields.io/pypi/v/certapi.svg)](https://pypi.org/project/certapi/)
33
+
31
34
 
32
35
  For a detailed list of changes, please refer to the [CHANGELOG.md](CHANGELOG.md).
33
36
 
@@ -39,7 +42,7 @@ You can install CertApi using pip
39
42
  pip install certapi
40
43
  ```
41
44
 
42
- ## Example: Obtain Certificate with Cloudflare
45
+ ## Example: Low Leve API : Certificate with Cloudflare
43
46
 
44
47
  ```python
45
48
  import json
@@ -51,7 +54,7 @@ from certapi import CertApiException, CloudflareChallengeSolver, Key, AcmeCertIs
51
54
  challenge_solver = CloudflareChallengeSolver(api_key=None)
52
55
 
53
56
  ## initialize cert issuer with a new account key
54
- cert_issuer = AcmeCertIssuer(Key.generate('rsa'), challenge_solver)
57
+ cert_issuer = AcmeCertIssuer(Key.generate('ecdsa'), challenge_solver)
55
58
 
56
59
  # Preform setup i.e. fetching directory and registering ACME account
57
60
  cert_issuer.setup()
@@ -70,7 +73,45 @@ except CertApiException as e:
70
73
  ```
71
74
 
72
75
 
73
- ## Example: Use High Leve API
76
+ ## Example: High Level API (with AcmeCertManager)
74
77
 
75
- ```
78
+ The `AcmeCertManager` provides a high-level interface that handles certificate storage, automatic renewal checks, and multi-solver management.
79
+
80
+ ```python
81
+ from certapi import (
82
+ AcmeCertManager,
83
+ FileSystemKeyStore,
84
+ AcmeCertIssuer,
85
+ CloudflareChallengeSolver
86
+ )
87
+
88
+ # 1. Setup KeyStore to persist keys and certificates
89
+ key_store = FileSystemKeyStore("db")
90
+
91
+
92
+ # DNS-01 via Cloudflare (e.g. for wildcard certs or internal domains)
93
+ dns_solver = CloudflareChallengeSolver(api_token="your-cloudflare-token")
94
+
95
+ # 3. Initialize and Setup AcmeCertManager
96
+ # Create cert issuer with the default challenge solver
97
+ cert_issuer = AcmeCertIssuer.with_keystore(key_store, dns_solver)
98
+
99
+ cert_manager = AcmeCertManager(
100
+ key_store=key_store,
101
+ cert_issuer=cert_issuer,
102
+ challenge_solvers=[dns_solver], # other solvers can be used
103
+ renew_threshold_days=7
104
+ )
105
+ cert_manager.setup()
106
+
107
+ # 4. Issue or Reuse Certificate
108
+ # Automatically checks keystore and renews only if necessary
109
+ response = cert_manager.issue_certificate(["example.com", "www.example.com"])
110
+
111
+ for cert_data in response.issued:
112
+ print(f"Newly issued for: {cert_data.domains}")
113
+ print(cert_data.cert)
114
+
115
+ for cert_data in response.existing:
116
+ print(f"Reusing existing for: {cert_data.domains}")
76
117
  ```
@@ -0,0 +1,93 @@
1
+ # CertApi
2
+
3
+ CertApi is a Python package for requesting SSL certificates from ACME.
4
+ This is to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
5
+
6
+ [![Build Status](https://github.com/mesudip/certapi/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
7
+ [![codecov](https://codecov.io/github/mesudip/certapi/graph/badge.svg?token=NYTNCH29IT)](https://codecov.io/github/mesudip/certapi)
8
+ [![PyPI version](https://img.shields.io/pypi/v/certapi.svg)](https://pypi.org/project/certapi/)
9
+
10
+
11
+ For a detailed list of changes, please refer to the [CHANGELOG.md](CHANGELOG.md).
12
+
13
+ ## Installation
14
+
15
+ You can install CertApi using pip
16
+
17
+ ```bash
18
+ pip install certapi
19
+ ```
20
+
21
+ ## Example: Low Leve API : Certificate with Cloudflare
22
+
23
+ ```python
24
+ import json
25
+ from certapi import CertApiException, CloudflareChallengeSolver, Key, AcmeCertIssuer
26
+
27
+
28
+ # Initialize the Cloudflare challenge solver
29
+ # The API key is read from the CLOUDFLARE_API_KEY environment variable, or you can set it below.
30
+ challenge_solver = CloudflareChallengeSolver(api_key=None)
31
+
32
+ ## initialize cert issuer with a new account key
33
+ cert_issuer = AcmeCertIssuer(Key.generate('ecdsa'), challenge_solver)
34
+
35
+ # Preform setup i.e. fetching directory and registering ACME account
36
+ cert_issuer.setup()
37
+
38
+ try:
39
+ # Obtain a certificate for your domain
40
+ (key, cert) = cert_issuer.generate_key_and_cert_for_domain("your-domain.com")
41
+
42
+ print("------ Private Key -----")
43
+ print(key.to_pem())
44
+ print("------- Certificate ------")
45
+ print(cert)
46
+ except CertApiException as e:
47
+ print(f"An error occurred:", json.dumps(e.json_obj(), indent=2))
48
+
49
+ ```
50
+
51
+
52
+ ## Example: High Level API (with AcmeCertManager)
53
+
54
+ The `AcmeCertManager` provides a high-level interface that handles certificate storage, automatic renewal checks, and multi-solver management.
55
+
56
+ ```python
57
+ from certapi import (
58
+ AcmeCertManager,
59
+ FileSystemKeyStore,
60
+ AcmeCertIssuer,
61
+ CloudflareChallengeSolver
62
+ )
63
+
64
+ # 1. Setup KeyStore to persist keys and certificates
65
+ key_store = FileSystemKeyStore("db")
66
+
67
+
68
+ # DNS-01 via Cloudflare (e.g. for wildcard certs or internal domains)
69
+ dns_solver = CloudflareChallengeSolver(api_token="your-cloudflare-token")
70
+
71
+ # 3. Initialize and Setup AcmeCertManager
72
+ # Create cert issuer with the default challenge solver
73
+ cert_issuer = AcmeCertIssuer.with_keystore(key_store, dns_solver)
74
+
75
+ cert_manager = AcmeCertManager(
76
+ key_store=key_store,
77
+ cert_issuer=cert_issuer,
78
+ challenge_solvers=[dns_solver], # other solvers can be used
79
+ renew_threshold_days=7
80
+ )
81
+ cert_manager.setup()
82
+
83
+ # 4. Issue or Reuse Certificate
84
+ # Automatically checks keystore and renews only if necessary
85
+ response = cert_manager.issue_certificate(["example.com", "www.example.com"])
86
+
87
+ for cert_data in response.issued:
88
+ print(f"Newly issued for: {cert_data.domains}")
89
+ print(cert_data.cert)
90
+
91
+ for cert_data in response.existing:
92
+ print(f"Reusing existing for: {cert_data.domains}")
93
+ ```
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="certapi",
5
- version="0.6.0",
5
+ version="1.0.1",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -21,3 +21,4 @@ from .challenge_solver import (
21
21
  DigitalOceanChallengeSolver,
22
22
  )
23
23
  from .issuers import CertIssuer, SelfCertIssuer, AcmeCertIssuer
24
+ from .client import CertManagerClient
@@ -28,16 +28,17 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
28
28
 
29
29
  def __init__(self, response: requests.Response, step: str):
30
30
  requests.HTTPError.__init__(self, response=response)
31
- self.response = response
32
31
  (message, detail) = self.extract_acme_response_error()
33
32
  AcmeError.__init__(self, message, detail, step)
33
+ self.response = response
34
+
34
35
 
35
36
  def extract_acme_response_error(self):
36
37
  message = None
37
38
  error = None
38
39
  try:
39
40
  res_json = self.response.json()
40
- if res_json["status"] == "invalid":
41
+ if res_json.get("status") == "invalid":
41
42
  if res_json["challenges"]:
42
43
  failed_challenge: dict = [x for x in res_json["challenges"] if x["status"] == "invalid"][0]
43
44
  error = failed_challenge["error"]
@@ -108,10 +109,7 @@ class AcmeHttpError(AcmeError, requests.HTTPError):
108
109
  hostname = error.get("hostname", "unknown")
109
110
  status = error.get("status", "unknown")
110
111
  message = (
111
- str(hostname)
112
- + " Status="
113
- + str(status)
114
- + ": Invalid response in challenge url"
112
+ str(hostname) + " Status=" + str(status) + ": Invalid response in challenge url"
115
113
  )
116
114
  else:
117
115
  message = err_detail
@@ -143,4 +141,4 @@ class AcmeInvalidNonceError(AcmeHttpError):
143
141
  def __init__(self, response: requests.Response, step: str):
144
142
  super().__init__(response, step)
145
143
  self.can_retry = True
146
- self.response=0
144
+ self.response = 0
@@ -0,0 +1 @@
1
+ from .cert_manager_client import CertManagerClient
@@ -80,7 +80,7 @@ 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
- retry_delay = e.retry_delay if hasattr(e, 'retry_delay') else delay
83
+ retry_delay = e.retry_delay if hasattr(e, "retry_delay") else delay
84
84
  print(f"Retrying {method} {url} in {retry_delay} seconds due to retryable error: {e.message}")
85
85
  time.sleep(retry_delay)
86
86
  else:
@@ -21,6 +21,16 @@ class AcmeCertIssuer(CertIssuer):
21
21
  self.challenge_solver = challenge_solver
22
22
  self.self_verify_challenge = self_verify_challenge
23
23
 
24
+ @staticmethod
25
+ def with_keystore(
26
+ key_store: "KeyStore",
27
+ challenge_solver: ChallengeSolver,
28
+ account_key_name: str = "acme_account.key",
29
+ acme_url: str = None,
30
+ ) -> "AcmeCertIssuer":
31
+ account_key, _ = key_store._get_or_generate_key(account_key_name)
32
+ return AcmeCertIssuer(account_key, challenge_solver, acme_url=acme_url)
33
+
24
34
  def setup(self):
25
35
  self.acme.setup()
26
36
  res: Response = self.acme.register()
@@ -12,6 +12,8 @@ from ..keystore.KeyStore import KeyStore
12
12
  from cryptography.x509 import Certificate, CertificateSigningRequest
13
13
  from ..crypto import Key, certs_to_pem, cert_to_pem, get_csr_hostnames
14
14
 
15
+ DEFAULT_RENEW_THRESHOLD_DAYS = 62
16
+
15
17
 
16
18
  class AcmeCertManager:
17
19
  def __init__(
@@ -19,10 +21,14 @@ class AcmeCertManager:
19
21
  key_store: KeyStore,
20
22
  cert_issuer: AcmeCertIssuer,
21
23
  challenge_solvers: List[ChallengeSolver] = [],
24
+ renew_threshold_days: int = DEFAULT_RENEW_THRESHOLD_DAYS, # Renewal will be accepted if cert is valid for less than 75 days
22
25
  ):
23
26
  self.key_store: KeyStore = key_store
24
27
  self.cert_issuer: AcmeCertIssuer = cert_issuer
25
28
  self.challenge_solvers: List[ChallengeSolver] = challenge_solvers
29
+ self.renew_threshold_days: int = (
30
+ DEFAULT_RENEW_THRESHOLD_DAYS if renew_threshold_days is None else renew_threshold_days
31
+ )
26
32
 
27
33
  def setup(self):
28
34
  names = [solver.__class__.__name__.replace("ChallengeSolver", "") for solver in self.challenge_solvers]
@@ -67,6 +73,7 @@ class AcmeCertManager:
67
73
  locality: Optional[str] = None,
68
74
  organization: Optional[str] = None,
69
75
  user_id: Optional[str] = None,
76
+ renew_threshold_days: Optional[int] = None,
70
77
  ) -> CertificateResponse:
71
78
 
72
79
  if type(hosts) == str:
@@ -77,10 +84,11 @@ class AcmeCertManager:
77
84
  result = self.key_store.find_key_and_cert_by_domain(h)
78
85
  if result is not None:
79
86
  # result is (domain_id, key, cert_list)
80
- cert=result[2][0]
87
+ cert = result[2][0]
81
88
  invalid_date = cert.not_valid_after_utc
82
- # Check if the certificate is still valid for at least 30 days
83
- if invalid_date > datetime.now(timezone.utc) + timedelta(days=6):
89
+ # Check if the certificate is still valid for at least renew_threshold_days
90
+ threshold = renew_threshold_days if renew_threshold_days is not None else self.renew_threshold_days
91
+ if invalid_date > datetime.now(timezone.utc) + timedelta(days=threshold):
84
92
  existing[h] = result
85
93
  missing = [h for h in hosts if h not in existing]
86
94
  if len(missing) > 0:
@@ -1,4 +1,6 @@
1
1
  import json
2
+ import threading
3
+ import os
2
4
  from flask import request, jsonify
3
5
  from cryptography.x509 import CertificateSigningRequest
4
6
  from flask_restx import Resource, reqparse, fields
@@ -6,7 +8,48 @@ from flask_restx import Resource, reqparse, fields
6
8
  from certapi import AcmeCertManager
7
9
 
8
10
 
9
- def create_api_resources(api_ns, cert_manager: AcmeCertManager):
11
+ class RenewalQueueFullError(Exception):
12
+ """Exception raised when the renewal queue is full."""
13
+
14
+ pass
15
+
16
+
17
+ class RenewalLockManager:
18
+ def __init__(self, queue_size: int = 5):
19
+ self.queue_size = queue_size
20
+ self._lock = threading.Lock()
21
+ self._condition = threading.Condition(self._lock)
22
+ self._renewing_domains = set()
23
+ self._waiting_threads = 0
24
+
25
+ def acquire(self, domains):
26
+ while True:
27
+ with self._lock:
28
+ # Check if any domain is already being renewed by another thread
29
+ if any(h in self._renewing_domains for h in domains):
30
+ if self._waiting_threads >= self.queue_size:
31
+ raise RenewalQueueFullError("Proper queue is full")
32
+
33
+ self._waiting_threads += 1
34
+ self._condition.wait()
35
+ self._waiting_threads -= 1
36
+ continue # Re-check everything after waking up
37
+
38
+ # Mark as renewing
39
+ for h in domains:
40
+ self._renewing_domains.add(h)
41
+ break
42
+
43
+ def release(self, domains):
44
+ with self._lock:
45
+ for h in domains:
46
+ self._renewing_domains.discard(h)
47
+ self._condition.notify_all()
48
+
49
+
50
+ def create_api_resources(api_ns, cert_manager: AcmeCertManager, renew_queue_size: int = 5):
51
+
52
+ lock_manager = RenewalLockManager(queue_size=renew_queue_size)
10
53
 
11
54
  # Models for documentation
12
55
  issued_cert_model = api_ns.model(
@@ -45,6 +88,7 @@ def create_api_resources(api_ns, cert_manager: AcmeCertManager):
45
88
  obtain_parser.add_argument("locality", type=str, help="Locality name")
46
89
  obtain_parser.add_argument("organization", type=str, help="Organization name")
47
90
  obtain_parser.add_argument("user_id", type=str, help="User ID")
91
+ obtain_parser.add_argument("renew_threshold_days", type=int, help="Threshold in days for certificate reuse")
48
92
 
49
93
  @api_ns.route("/obtain")
50
94
  class ObtainCert(Resource):
@@ -59,16 +103,23 @@ def create_api_resources(api_ns, cert_manager: AcmeCertManager):
59
103
  args = obtain_parser.parse_args()
60
104
  hostnames = args["hostname"]
61
105
 
62
- data = cert_manager.issue_certificate(
63
- hostnames,
64
- key_type=args["key_type"],
65
- expiry_days=args["expiry_days"],
66
- country=args["country"],
67
- state=args["state"],
68
- locality=args["locality"],
69
- organization=args["organization"],
70
- user_id=args["user_id"],
71
- )
106
+ # Acquire lock for the domains
107
+ lock_manager.acquire(hostnames)
108
+ try:
109
+ data = cert_manager.issue_certificate(
110
+ hostnames,
111
+ key_type=args["key_type"],
112
+ expiry_days=args["expiry_days"],
113
+ country=args["country"],
114
+ state=args["state"],
115
+ locality=args["locality"],
116
+ organization=args["organization"],
117
+ user_id=args["user_id"],
118
+ renew_threshold_days=args.get("renew_threshold_days"),
119
+ )
120
+ finally:
121
+ lock_manager.release(hostnames)
122
+
72
123
  print(data)
73
124
  print(data.to_json())
74
125
  if data:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: certapi
3
- Version: 0.6.0
3
+ Version: 1.0.1
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
@@ -27,7 +27,10 @@ Dynamic: summary
27
27
  CertApi is a Python package for requesting SSL certificates from ACME.
28
28
  This is to be used as a base library for building other tools, or to integrate Certificate creation feature in you app.
29
29
 
30
- > ⚠️ Warning: This project is in beta. Please stay tuned for the LTS `v1.0.0` release.
30
+ [![Build Status](https://github.com/mesudip/certapi/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
31
+ [![codecov](https://codecov.io/github/mesudip/certapi/graph/badge.svg?token=NYTNCH29IT)](https://codecov.io/github/mesudip/certapi)
32
+ [![PyPI version](https://img.shields.io/pypi/v/certapi.svg)](https://pypi.org/project/certapi/)
33
+
31
34
 
32
35
  For a detailed list of changes, please refer to the [CHANGELOG.md](CHANGELOG.md).
33
36
 
@@ -39,7 +42,7 @@ You can install CertApi using pip
39
42
  pip install certapi
40
43
  ```
41
44
 
42
- ## Example: Obtain Certificate with Cloudflare
45
+ ## Example: Low Leve API : Certificate with Cloudflare
43
46
 
44
47
  ```python
45
48
  import json
@@ -51,7 +54,7 @@ from certapi import CertApiException, CloudflareChallengeSolver, Key, AcmeCertIs
51
54
  challenge_solver = CloudflareChallengeSolver(api_key=None)
52
55
 
53
56
  ## initialize cert issuer with a new account key
54
- cert_issuer = AcmeCertIssuer(Key.generate('rsa'), challenge_solver)
57
+ cert_issuer = AcmeCertIssuer(Key.generate('ecdsa'), challenge_solver)
55
58
 
56
59
  # Preform setup i.e. fetching directory and registering ACME account
57
60
  cert_issuer.setup()
@@ -70,7 +73,45 @@ except CertApiException as e:
70
73
  ```
71
74
 
72
75
 
73
- ## Example: Use High Leve API
76
+ ## Example: High Level API (with AcmeCertManager)
74
77
 
75
- ```
78
+ The `AcmeCertManager` provides a high-level interface that handles certificate storage, automatic renewal checks, and multi-solver management.
79
+
80
+ ```python
81
+ from certapi import (
82
+ AcmeCertManager,
83
+ FileSystemKeyStore,
84
+ AcmeCertIssuer,
85
+ CloudflareChallengeSolver
86
+ )
87
+
88
+ # 1. Setup KeyStore to persist keys and certificates
89
+ key_store = FileSystemKeyStore("db")
90
+
91
+
92
+ # DNS-01 via Cloudflare (e.g. for wildcard certs or internal domains)
93
+ dns_solver = CloudflareChallengeSolver(api_token="your-cloudflare-token")
94
+
95
+ # 3. Initialize and Setup AcmeCertManager
96
+ # Create cert issuer with the default challenge solver
97
+ cert_issuer = AcmeCertIssuer.with_keystore(key_store, dns_solver)
98
+
99
+ cert_manager = AcmeCertManager(
100
+ key_store=key_store,
101
+ cert_issuer=cert_issuer,
102
+ challenge_solvers=[dns_solver], # other solvers can be used
103
+ renew_threshold_days=7
104
+ )
105
+ cert_manager.setup()
106
+
107
+ # 4. Issue or Reuse Certificate
108
+ # Automatically checks keystore and renews only if necessary
109
+ response = cert_manager.issue_certificate(["example.com", "www.example.com"])
110
+
111
+ for cert_data in response.issued:
112
+ print(f"Newly issued for: {cert_data.domains}")
113
+ print(cert_data.cert)
114
+
115
+ for cert_data in response.existing:
116
+ print(f"Reusing existing for: {cert_data.domains}")
76
117
  ```
@@ -52,6 +52,7 @@ src/certapi/server/__init__.py
52
52
  src/certapi/server/api.py
53
53
  src/certapi/server/cert_api.py
54
54
  src/certapi/server/key_api.py
55
+ tests/test_acme_error_handling.py
55
56
  tests/test_cert_issuer_generic.py
56
57
  tests/test_certs_with_key_types.py
57
58
  tests/test_http_error_handling.py