Repository: certbot/certbot

Issue:
acme: no interface for asynchronous finalization a la validation
`acme` provides API for answering challenges and later polling the status of authorizations. It would be very helpful for our use case if similar API existed for finalization (separating "start finalization" from "poll for completion of finalization"), which would essentially just entail splitting `acme.client.ClientV2.finalize_order` into two methods (one for starting finalization and one for polling for status) that `finalize_order` would call (to avoid breaking API). I'm more than happy to do the refactor myself if there's appetite for a patch doing this; is this something the project would review and take?


Discussion:
👍 This sounds like a good idea and I'd be happy to review it.

Resolving patch:
diff --git a/acme/acme/client.py b/acme/acme/client.py
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -210,23 +210,35 @@ def poll_authorizations(self, orderr: messages.OrderResource, deadline: datetime
             raise errors.ValidationError(failed)
         return orderr.update(authorizations=responses)
 
-    def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.datetime,
-                       fetch_alternative_chains: bool = False) -> messages.OrderResource:
-        """Finalize an order and obtain a certificate.
+    def begin_finalization(self, orderr: messages.OrderResource
+                           ) -> messages.OrderResource:
+        """Start the process of finalizing an order.
 
         :param messages.OrderResource orderr: order to finalize
         :param datetime.datetime deadline: when to stop polling and timeout
-        :param bool fetch_alternative_chains: whether to also fetch alternative
-            certificate chains
 
-        :returns: finalized order
+        :returns: updated order
         :rtype: messages.OrderResource
-
         """
         csr = OpenSSL.crypto.load_certificate_request(
             OpenSSL.crypto.FILETYPE_PEM, orderr.csr_pem)
         wrapped_csr = messages.CertificateRequest(csr=jose.ComparableX509(csr))
-        self._post(orderr.body.finalize, wrapped_csr)
+        res = self._post(orderr.body.finalize, wrapped_csr)
+        orderr = orderr.update(body=messages.Order.from_json(res.json()))
+        return orderr
+
+    def poll_finalization(self, orderr: messages.OrderResource,
+                          deadline: datetime.datetime,
+                          fetch_alternative_chains: bool = False
+                          ) -> messages.OrderResource:
+        """
+        Poll an order that has been finalized for its status.
+        If it becomes valid, obtain the certificate.
+
+        :returns: finalized order (with certificate)
+        :rtype: messages.OrderResource
+        """
+
         while datetime.datetime.now() < deadline:
             time.sleep(1)
             response = self._post_as_get(orderr.uri)
@@ -247,6 +259,22 @@ def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.date
                 return orderr
         raise errors.TimeoutError()
 
+    def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.datetime,
+                       fetch_alternative_chains: bool = False) -> messages.OrderResource:
+        """Finalize an order and obtain a certificate.
+
+        :param messages.OrderResource orderr: order to finalize
+        :param datetime.datetime deadline: when to stop polling and timeout
+        :param bool fetch_alternative_chains: whether to also fetch alternative
+            certificate chains
+
+        :returns: finalized order
+        :rtype: messages.OrderResource
+
+        """
+        self.begin_finalization(orderr)
+        return self.poll_finalization(orderr, deadline, fetch_alternative_chains)
+
     def revoke(self, cert: jose.ComparableX509, rsn: int) -> None:
         """Revoke certificate.
 


Test patch: