certapi 0.4.3__tar.gz → 0.4.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 (56) hide show
  1. {certapi-0.4.3 → certapi-0.4.4}/PKG-INFO +1 -1
  2. {certapi-0.4.3 → certapi-0.4.4}/setup.py +1 -1
  3. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/__init__.py +2 -2
  4. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/AcmeError.py +3 -9
  5. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/ChallengeSolver.py +4 -0
  6. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +7 -1
  7. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +1 -1
  8. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +35 -5
  9. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +38 -9
  10. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +31 -4
  11. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +35 -10
  12. certapi-0.4.4/src/certapi/errors.py +17 -0
  13. {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/PKG-INFO +1 -1
  14. {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/SOURCES.txt +1 -0
  15. {certapi-0.4.3 → certapi-0.4.4}/MANIFEST.in +0 -0
  16. {certapi-0.4.3 → certapi-0.4.4}/README.md +0 -0
  17. {certapi-0.4.3 → certapi-0.4.4}/pyproject.toml +0 -0
  18. {certapi-0.4.3 → certapi-0.4.4}/setup.cfg +0 -0
  19. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/Acme.py +0 -0
  20. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/Challenge.py +0 -0
  21. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/Order.py +0 -0
  22. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/__init__.py +0 -0
  23. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/acme/http.py +0 -0
  24. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/__init__.py +0 -0
  25. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/__init__.py +0 -0
  26. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
  27. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
  28. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/client/__init__.py +0 -0
  29. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/client/cert_manager_client.py +0 -0
  30. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/crypto/__init__.py +0 -0
  31. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/crypto/crypto.py +0 -0
  32. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/crypto/crypto_classes.py +0 -0
  33. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/http/__init__.py +0 -0
  34. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/http/client.py +0 -0
  35. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/http/types.py +0 -0
  36. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/issuers/AcmeCertIssuer.py +0 -0
  37. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/issuers/SelfCertIssuer.py +0 -0
  38. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/issuers/__init__.py +0 -0
  39. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/issuers/abstract_certissuer.py +0 -0
  40. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/FileSystemKeystore.py +0 -0
  41. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/KeyStore.py +0 -0
  42. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
  43. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/RemoteKeyStore.py +0 -0
  44. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/SqliteKeyStore.py +0 -0
  45. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/keystore/__init__.py +0 -0
  46. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/manager/__init__.py +0 -0
  47. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/manager/acme_cert_manager.py +0 -0
  48. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/server/__init__.py +0 -0
  49. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/server/api.py +0 -0
  50. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/server/cert_api.py +0 -0
  51. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/server/key_api.py +0 -0
  52. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/util.py +0 -0
  53. {certapi-0.4.3 → certapi-0.4.4}/src/certapi/utils.py +0 -0
  54. {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/dependency_links.txt +0 -0
  55. {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/requires.txt +0 -0
  56. {certapi-0.4.3 → certapi-0.4.4}/src/certapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: certapi
3
- Version: 0.4.3
3
+ Version: 0.4.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="0.4.3",
5
+ version="0.4.4",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -1,7 +1,7 @@
1
1
  from .acme.Acme import Acme, Order, AcmeNetworkError, AcmeHttpError, Challenge
2
2
  from .manager.acme_cert_manager import AcmeCertManager
3
3
  from .http.types import CertificateResponse, IssuedCert
4
-
4
+ from .errors import CertApiException
5
5
  from .crypto import (
6
6
  Certificate,
7
7
  CertificateSigningRequest,
@@ -20,4 +20,4 @@ from .challenge_solver import (
20
20
  CloudflareChallengeSolver,
21
21
  DigitalOceanChallengeSolver,
22
22
  )
23
- from .issuers import SelfCertIssuer
23
+ from .issuers import CertIssuer,SelfCertIssuer,AcmeCertIssuer
@@ -1,18 +1,13 @@
1
1
  import re
2
2
  import requests
3
+ from certapi.errors import CertApiException
3
4
 
4
5
 
5
- class AcmeError(Exception):
6
+ class AcmeError(CertApiException):
6
7
  def __init__(self, message, detail, step):
7
- super().__init__(step, message)
8
- self.message: str = message
9
- self.step: str = step
10
- self.detail: dict = detail
8
+ super().__init__(message, detail, step)
11
9
  self.can_retry = False
12
10
 
13
- def json_obj(self) -> dict:
14
- return {"message": self.message, "step": self.step, "detail": self.detail}
15
-
16
11
 
17
12
  class AcmeNetworkError(AcmeError, requests.RequestException):
18
13
  """
@@ -23,7 +18,6 @@ class AcmeNetworkError(AcmeError, requests.RequestException):
23
18
  # Initialize both parent classes
24
19
  requests.RequestException.__init__(self, request=request)
25
20
  AcmeError.__init__(self, message, detail, step)
26
- requests.RequestException.__init__(self, request=request) # Pass message to RequestException
27
21
  self.can_retry = True
28
22
 
29
23
 
@@ -29,6 +29,10 @@ class ChallengeSolver(ABC):
29
29
  def supported_challenge_type(self) -> Literal["http-01", "dns-01", "tls-alpn-01"]:
30
30
  pass
31
31
 
32
+ @abstractmethod
33
+ def cleanup_old_challenges(self):
34
+ pass
35
+
32
36
  def __iter__(self):
33
37
  raise NotImplementedError("Must implement `__iter__` method.")
34
38
 
@@ -5,7 +5,7 @@ import os
5
5
 
6
6
  class FilesystemChallengeSolver(ChallengeSolver):
7
7
  """
8
- Filesystem implementation of the ChallengeStore.
8
+ Filesystem implementation of the ChallengeSolver.
9
9
  This should never be used, but extended for your webserver e.g. nginx, apache etc.
10
10
  """
11
11
 
@@ -36,6 +36,12 @@ class FilesystemChallengeSolver(ChallengeSolver):
36
36
  def supported_challenge_type(self) -> Literal["http-01"]:
37
37
  return "http-01"
38
38
 
39
+ def cleanup_old_challenges(self):
40
+ for filename in os.listdir(self.directory):
41
+ file_path = os.path.join(self.directory, filename)
42
+ if os.path.isfile(file_path):
43
+ os.remove(file_path)
44
+
39
45
  def __iter__(self):
40
46
  return (f for f in os.listdir(self.directory) if os.path.isfile(os.path.join(self.directory, f)))
41
47
 
@@ -4,7 +4,7 @@ from .ChallengeSolver import ChallengeSolver
4
4
 
5
5
  class InMemoryChallengeSolver(ChallengeSolver):
6
6
  """
7
- In-memory implementation of the ChallengeStore.
7
+ In-memory implementation of the ChallengeSolver.
8
8
  """
9
9
 
10
10
  def __init__(self):
@@ -2,8 +2,10 @@ import os
2
2
  import time
3
3
  from collections.abc import MutableMapping
4
4
  from typing import Literal
5
+ from concurrent.futures import ThreadPoolExecutor
5
6
  from ...ChallengeSolver import ChallengeSolver
6
7
  from .cloudflare_client import Cloudflare
8
+ from certapi.errors import CertApiException
7
9
 
8
10
 
9
11
  class CloudflareChallengeSolver(ChallengeSolver):
@@ -22,7 +24,10 @@ class CloudflareChallengeSolver(ChallengeSolver):
22
24
  try:
23
25
  self.cloudflare.determine_registered_domain(domain)
24
26
  return True
25
- except Exception:
27
+ except CertApiException as e:
28
+ # Log the exception or handle it as needed, but return False as it doesn't support the domain
29
+ print(f"CloudflareChallengeSolver.supports_domain: {e.message} - {e.detail}")
30
+ return False
26
31
  return False
27
32
 
28
33
  def save_challenge(self, key: str, value: str, domain=None):
@@ -32,7 +37,7 @@ class CloudflareChallengeSolver(ChallengeSolver):
32
37
 
33
38
  record_id = self.cloudflare.create_record(name=key, data=value, domain=base_domain)
34
39
  self.challenges_map[key] = record_id
35
- print(f"CloudflareChallengeStore[{domain}]: Added Record {key}")
40
+ print(f"CloudflareChallengeSolver[{domain}]: Added Record {key}")
36
41
 
37
42
  def get_challenge(self, key: str, domain: str) -> str:
38
43
  base_domain = self.cloudflare.determine_registered_domain(domain)
@@ -40,17 +45,42 @@ class CloudflareChallengeSolver(ChallengeSolver):
40
45
  for record in records:
41
46
  if record["name"] == key:
42
47
  return record["content"]
43
- return None # Return None if not found, as per ChallengeStore's __getitem__ behavior
48
+ return None # Return None if not found, as per ChallengeSolver's __getitem__ behavior
44
49
 
45
50
  def delete_challenge(self, key: str, domain: str):
46
51
  if key not in self.challenges_map:
47
- raise KeyError(f"Challenge {key} not found in store (no record_id stored).")
52
+ print(f"CloudflareChallengeSolver.delete: Not found Skipping key={key} record_id={record_id}")
53
+ return
48
54
 
49
55
  record_id = self.challenges_map[key]
50
56
  base_domain = self.cloudflare.determine_registered_domain(domain)
51
57
  self.cloudflare.delete_record(record=record_id, domain=base_domain)
52
58
  del self.challenges_map[key]
53
- print(f"CloudflareChallengeStore: Deleted challenge for {key} with record ID {record_id}")
59
+ print(f"CloudflareChallengeSolver: Deleted challenge for {key} with record ID {record_id}")
60
+
61
+ def _cleanup_zone_challenges(self, zone):
62
+ zone_name = zone["name"]
63
+ try:
64
+ # List all TXT records in the zone
65
+ records = self.cloudflare.list_txt_records(zone_name)
66
+ for record in records:
67
+ if record["type"] == "TXT" and record["name"].startswith("_acme-challenge"):
68
+ print(f"CloudflareChallengeSolver: Deleting old challenge record {record['name']} in zone {zone_name}")
69
+ try:
70
+ self.cloudflare.delete_record(record["id"], zone_name)
71
+ except CertApiException as e:
72
+ print(f"CloudflareChallengeSolver: Warning - Failed to delete record {record['name']} in zone {zone_name}: {e.message} - {e.detail}")
73
+ except Exception as e:
74
+ print(f"CloudflareChallengeSolver: Warning - An unexpected error occurred while deleting record {record['name']} in zone {zone_name}: {e}")
75
+ except CertApiException as e:
76
+ print(f"CloudflareChallengeSolver: Error listing challenges in zone {zone_name}: {e.message} - {e.detail}")
77
+ except Exception as e:
78
+ print(f"CloudflareChallengeSolver: An unexpected error occurred while listing challenges in zone {zone_name}: {e}")
79
+
80
+ def cleanup_old_challenges(self):
81
+ zones = self.cloudflare._get_zones()
82
+ with ThreadPoolExecutor(max_workers=len(zones)) as executor:
83
+ executor.map(self._cleanup_zone_challenges, zones)
54
84
 
55
85
  def __iter__(self):
56
86
  # This is tricky as we can't easily iterate all challenges across all domains
@@ -2,6 +2,7 @@ import json
2
2
  import time
3
3
  from os import getenv
4
4
  from urllib.request import urlopen, Request
5
+ from certapi.errors import CertApiException
5
6
 
6
7
 
7
8
  class Cloudflare(object):
@@ -13,7 +14,7 @@ class Cloudflare(object):
13
14
  if not self.token:
14
15
  self.token = getenv("CLOUDFLARE_API_KEY")
15
16
  if not self.token:
16
- raise Exception("CLOUDFLARE_API_KEY not found in environment")
17
+ raise CertApiException("CLOUDFLARE_API_KEY not found in environment", step="Cloudflare.__init__")
17
18
 
18
19
  self._zones_cache = None
19
20
  self._zones_cache_time = 0 # Unix timestamp of last cache update
@@ -31,7 +32,11 @@ class Cloudflare(object):
31
32
  api_url = "{0}/zones?per_page=50".format(self.api)
32
33
  response = urlopen(Request(api_url, headers=request_headers))
33
34
  if response.getcode() != 200:
34
- raise Exception(json.loads(response.read().decode("utf8")))
35
+ raise CertApiException(
36
+ "Cloudflare API error",
37
+ detail=json.loads(response.read().decode("utf8")),
38
+ step="Cloudflare._get_zones"
39
+ )
35
40
 
36
41
  zones = json.loads(response.read().decode("utf8"))["result"]
37
42
  self._zones_cache = zones
@@ -44,7 +49,11 @@ class Cloudflare(object):
44
49
  for zone in zones:
45
50
  if zone["name"] == domain:
46
51
  return zone["id"]
47
- raise Exception("No Cloudflare zone found for domain: {0}".format(domain))
52
+ raise CertApiException(
53
+ "No Cloudflare zone found for domain",
54
+ detail={"domain": domain},
55
+ step="Cloudflare._get_zone_id"
56
+ )
48
57
 
49
58
  def determine_registered_domain(self, domain: str) -> str:
50
59
  """
@@ -62,9 +71,13 @@ class Cloudflare(object):
62
71
  err = e
63
72
  continue
64
73
  if err:
65
- raise err
74
+ raise CertApiException(str(err), step="Cloudflare.determine_registered_domain")
66
75
  else:
67
- raise Exception("Could not determine Cloudflare registered domain for: {0}".format(domain))
76
+ raise CertApiException(
77
+ "Could not determine Cloudflare registered domain",
78
+ detail={"domain": domain},
79
+ step="Cloudflare.determine_registered_domain"
80
+ )
68
81
 
69
82
  def list_txt_records(self, domain: str, name_filter: str = None) -> list:
70
83
  """
@@ -81,12 +94,20 @@ class Cloudflare(object):
81
94
  response = urlopen(Request(api_url, headers=request_headers))
82
95
 
83
96
  if response.getcode() != 200:
84
- raise Exception(json.loads(response.read().decode("utf8")))
97
+ raise CertApiException(
98
+ "Cloudflare API error",
99
+ detail=json.loads(response.read().decode("utf8")),
100
+ step="Cloudflare.list_txt_records"
101
+ )
85
102
 
86
103
  result = json.loads(response.read().decode("utf8"))
87
104
  if not result.get("success"):
88
105
  print(f"List TXT record [{response.getcode()}]", result)
89
- raise Exception(result.get("errors", "Unknown error listing TXT records"))
106
+ raise CertApiException(
107
+ "Unknown error listing TXT records",
108
+ detail=result.get("errors", "Unknown error listing TXT records"),
109
+ step="Cloudflare.list_txt_records"
110
+ )
90
111
 
91
112
  return result["result"]
92
113
 
@@ -115,7 +136,11 @@ class Cloudflare(object):
115
136
  result = response.read().decode("utf8")
116
137
  if response.getcode() != 200:
117
138
  print(f"Create TXT record [{response.getcode()}]", result)
118
- raise Exception(json.loads(response.read().decode("utf8")))
139
+ raise CertApiException(
140
+ "Cloudflare API error",
141
+ detail=json.loads(result),
142
+ step="Cloudflare.create_record"
143
+ )
119
144
 
120
145
  return json.loads(result)["result"]["id"]
121
146
 
@@ -136,4 +161,8 @@ class Cloudflare(object):
136
161
  result = response.read().decode("utf8")
137
162
  if response.getcode() != 200:
138
163
  print(f"Delete dns record [{response.getcode()}]", result)
139
- raise Exception(json.loads(response.read().decode("utf8")))
164
+ raise CertApiException(
165
+ "Cloudflare API error",
166
+ detail=json.loads(result),
167
+ step="Cloudflare.delete_record"
168
+ )
@@ -3,6 +3,7 @@ from collections.abc import MutableMapping
3
3
  from typing import Literal
4
4
  from ...ChallengeSolver import ChallengeSolver
5
5
  from .digitalocean_client import DigitalOcean
6
+ from certapi.errors import CertApiException
6
7
 
7
8
 
8
9
  class DigitalOceanChallengeSolver(ChallengeSolver):
@@ -21,7 +22,13 @@ class DigitalOceanChallengeSolver(ChallengeSolver):
21
22
  try:
22
23
  self.digitalocean.determine_domain(domain)
23
24
  return True
24
- except Exception:
25
+ except CertApiException as e:
26
+ # Log the exception or handle it as needed, but return False as it doesn't support the domain
27
+ print(f"DigitalOceanChallengeSolver.supports_domain: {e.message} - {e.detail}")
28
+ return False
29
+ except Exception as e:
30
+ # Catch any other unexpected exceptions
31
+ print(f"DigitalOceanChallengeSolver.supports_domain: An unexpected error occurred: {e}")
25
32
  return False
26
33
 
27
34
  def save_challenge(self, key: str, value: str, domain=None):
@@ -31,7 +38,7 @@ class DigitalOceanChallengeSolver(ChallengeSolver):
31
38
 
32
39
  record_id = self.digitalocean.create_record(name=key, data=value, domain=base_domain)
33
40
  self.challenges_map[key] = record_id
34
- print(f"DigitalOceanChallengeStore: Saved challenge for {key} with record ID {record_id}")
41
+ print(f"DigitalOceanChallengeSolver: Saved challenge for {key} with record ID {record_id}")
35
42
 
36
43
  def get_challenge(self, key: str, domain: str) -> str:
37
44
  base_domain = self.digitalocean.determine_domain(domain)
@@ -39,7 +46,7 @@ class DigitalOceanChallengeSolver(ChallengeSolver):
39
46
  for record in records:
40
47
  if record["name"] == key:
41
48
  return record["data"] # DigitalOcean API returns 'data' for TXT record content
42
- return None # Return None if not found, as per ChallengeStore's __getitem__ behavior
49
+ return None # Return None if not found, as per ChallengeSolver's __getitem__ behavior
43
50
 
44
51
  def delete_challenge(self, key: str, domain: str):
45
52
  if key not in self.challenges_map:
@@ -49,7 +56,27 @@ class DigitalOceanChallengeSolver(ChallengeSolver):
49
56
  base_domain = self.digitalocean.determine_domain(domain)
50
57
  self.digitalocean.delete_record(record=record_id, domain=base_domain)
51
58
  del self.challenges_map[key]
52
- print(f"DigitalOceanChallengeStore: Deleted challenge for {key} with record ID {record_id}")
59
+ print(f"DigitalOceanChallengeSolver: Deleted challenge for {key} with record ID {record_id}")
60
+
61
+ def cleanup_old_challenges(self):
62
+ domains = self.digitalocean._get_domains()
63
+ for domain_obj in domains:
64
+ domain_name = domain_obj["name"]
65
+ try:
66
+ records = self.digitalocean.list_records(domain_name)
67
+ for record in records:
68
+ if record["type"] == "TXT" and record["name"].startswith("_acme-challenge"):
69
+ print(f"DigitalOceanChallengeSolver: Deleting old challenge record {record['name']} in domain {domain_name}")
70
+ try:
71
+ self.digitalocean.delete_record(record["id"], domain_name)
72
+ except CertApiException as e:
73
+ print(f"DigitalOceanChallengeSolver: Warning - Failed to delete record {record['name']} in domain {domain_name}: {e.message} - {e.detail}")
74
+ except Exception as e:
75
+ print(f"DigitalOceanChallengeSolver: Warning - An unexpected error occurred while deleting record {record['name']} in domain {domain_name}: {e}")
76
+ except CertApiException as e:
77
+ print(f"DigitalOceanChallengeSolver: Error listing challenges in domain {domain_name}: {e.message} - {e.detail}")
78
+ except Exception as e:
79
+ print(f"DigitalOceanChallengeSolver: An unexpected error occurred while listing challenges in domain {domain_name}: {e}")
53
80
 
54
81
  def __iter__(self):
55
82
  # This is tricky as we can't easily iterate all challenges across all domains
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  from os import getenv
3
-
4
- from urllib.request import urlopen, Request # Python 3
3
+ from urllib.request import urlopen, Request
4
+ from certapi.errors import CertApiException
5
5
 
6
6
 
7
7
  class DigitalOcean(object):
@@ -11,18 +11,31 @@ class DigitalOcean(object):
11
11
  if not self.token:
12
12
  self.token = getenv("DIGITALOCEAN_API_KEY")
13
13
  if not self.token:
14
- raise Exception("DIGITALOCEAN_API_KEY not found in environment")
14
+ raise CertApiException("DIGITALOCEAN_API_KEY not found in environment", step="DigitalOcean.__init__")
15
15
 
16
- def determine_domain(self, domain):
17
- """Determine registered domain in API"""
16
+ def _get_domains(self):
17
+ """Fetch DigitalOcean domains"""
18
18
  request_headers = {"Content-Type": "application/json", "Authorization": "Bearer {0}".format(self.token)}
19
19
  response = urlopen(Request(self.api, headers=request_headers))
20
20
  if response.getcode() != 200:
21
- raise Exception(json.loads(response.read().decode("utf8")))
22
- domains = json.loads(response.read().decode("utf8"))["domains"]
21
+ raise CertApiException(
22
+ "DigitalOcean API error",
23
+ detail=json.loads(response.read().decode("utf8")),
24
+ step="DigitalOcean._get_domains"
25
+ )
26
+ return json.loads(response.read().decode("utf8"))["domains"]
27
+
28
+ def determine_domain(self, domain):
29
+ """Determine registered domain in API"""
30
+ domains = self._get_domains()
23
31
  for d in domains:
24
32
  if d["name"] in domain:
25
33
  return d["name"]
34
+ raise CertApiException(
35
+ "No DigitalOcean domain found for: {0}".format(domain),
36
+ detail={"domain": domain},
37
+ step="DigitalOcean.determine_domain"
38
+ )
26
39
 
27
40
  def create_record(self, name, data, domain):
28
41
  """
@@ -40,7 +53,11 @@ class DigitalOcean(object):
40
53
  request_data = {"type": "TXT", "ttl": 300, "name": name, "data": data}
41
54
  response = urlopen(Request(api, data=json.dumps(request_data).encode("utf8"), headers=request_headers))
42
55
  if response.getcode() != 201:
43
- raise Exception(json.loads(response.read().decode("utf8")))
56
+ raise CertApiException(
57
+ "DigitalOcean API error",
58
+ detail=json.loads(response.read().decode("utf8")),
59
+ step="DigitalOcean.create_record"
60
+ )
44
61
  return json.loads(response.read().decode("utf8"))["domain_record"]["id"]
45
62
 
46
63
  def delete_record(self, record, domain):
@@ -57,7 +74,11 @@ class DigitalOcean(object):
57
74
  request.get_method = lambda: "DELETE"
58
75
  response = urlopen(request)
59
76
  if response.getcode() != 204:
60
- raise Exception(json.loads(response.read().decode("utf8")))
77
+ raise CertApiException(
78
+ "DigitalOcean API error",
79
+ detail=json.loads(response.read().decode("utf8")),
80
+ step="DigitalOcean.delete_record"
81
+ )
61
82
 
62
83
  def list_records(self, domain, name_filter=None):
63
84
  """
@@ -73,7 +94,11 @@ class DigitalOcean(object):
73
94
  request_headers = {"Content-Type": "application/json", "Authorization": "Bearer {0}".format(self.token)}
74
95
  response = urlopen(Request(api, headers=request_headers))
75
96
  if response.getcode() != 200:
76
- raise Exception(json.loads(response.read().decode("utf8")))
97
+ raise CertApiException(
98
+ "DigitalOcean API error",
99
+ detail=json.loads(response.read().decode("utf8")),
100
+ step="DigitalOcean.list_records"
101
+ )
77
102
 
78
103
  all_records = json.loads(response.read().decode("utf8"))["domain_records"]
79
104
 
@@ -0,0 +1,17 @@
1
+ class CertApiException(Exception):
2
+ """
3
+ Base exception for all CertAPI related errors.
4
+ """
5
+ def __init__(self, message: str, detail: dict = None, step: str = None):
6
+ super().__init__(message)
7
+ self.message = message
8
+ self.detail = detail if detail is not None else {}
9
+ self.step = step
10
+ self.can_retry = False
11
+
12
+ def json_obj(self) -> dict:
13
+ return {
14
+ "message": self.message,
15
+ "step": self.step,
16
+ "detail": self.detail
17
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: certapi
3
- Version: 0.4.3
3
+ Version: 0.4.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
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  setup.py
5
5
  src/certapi/__init__.py
6
+ src/certapi/errors.py
6
7
  src/certapi/util.py
7
8
  src/certapi/utils.py
8
9
  src/certapi.egg-info/PKG-INFO
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes