{"action":"created","comment":{"url":"https://api.github.com/repos/komuw/sewer/pulls/comments/172041360","pull_request_review_id":100998942,"id":172041360,"diff_hunk":"@@ -0,0 +1,646 @@\n+import time\n+import copy\n+import json\n+import base64\n+import hashlib\n+import binascii\n+import platform\n+\n+import requests\n+import OpenSSL\n+import cryptography\n+from structlog import get_logger\n+\n+from . import __version__ as sewer_version\n+\n+\n+class Client(object):\n+    \"\"\"\n+    todo: improve documentation.\n+\n+    usage:\n+        import sewer\n+        dns_class = sewer.CloudFlareDns(CLOUDFLARE_DNS_ZONE_ID='random',\n+                                        CLOUDFLARE_EMAIL='example@example.com',\n+                                        CLOUDFLARE_API_KEY='nsa-grade-api-key')\n+\n+        1. to create a new certificate.\n+        client = sewer.Client(domain_name='example.com',\n+                              dns_class=dns_class)\n+        certificate = client.cert()\n+        certificate_key = client.certificate_key\n+        account_key = client.account_key\n+\n+        with open('certificate.crt', 'w') as certificate_file:\n+            certificate_file.write(certificate)\n+\n+        with open('certificate.key', 'w') as certificate_key_file:\n+            certificate_key_file.write(certificate_key)\n+\n+\n+        2. to renew a certificate:\n+        with open('account_key.key', 'r') as account_key_file:\n+            account_key = account_key_file.read()\n+\n+        client = sewer.Client(domain_name='example.com',\n+                              dns_class=dns_class,\n+                              account_key=account_key)\n+        certificate = client.renew()\n+        certificate_key = client.certificate_key\n+\n+    todo:\n+        - handle more exceptions\n+    \"\"\"\n+\n+    def __init__(\n+            self,\n+            domain_name,\n+            dns_class,\n+            domain_alt_names=[],\n+            contact_email=None,\n+            account_key=None,\n+            bits=2048,\n+            digest='sha256',\n+            ACME_REQUEST_TIMEOUT=7,\n+            ACME_AUTH_STATUS_WAIT_PERIOD=8,\n+            ACME_AUTH_STATUS_MAX_CHECKS=3,\n+            ACME_DIRECTORY_URL='https://acme-staging-v02.api.letsencrypt.org/directory'):\n+        \"\"\"\n+        :param domain_name:                  (required) [string]\n+            the name that you want to acquire/renew certificate for. wildcards are allowed.\n+        :param dns_class:                    (required) [class]\n+            a subclass of sewer.common.BaseDns which will be called to create/delete DNS TXT records.\n+        :param domain_alt_names:             (optional) [list]\n+            list of alternative names that you want to be bundled into the same certificate as domain_name.\n+        :param contact_email:                (optional) [string]\n+            a contact email address\n+        :param account_key:                  (optional) [string]\n+            a string whose contents is an ssl certificate that identifies your account on the acme server.\n+            if you do not provide one, this client will issue a new certificate else will renew.\n+        :param bits:                         (optional) [integer]\n+            number of bits that will be used to create your certificates' private key.\n+        :param digest:                       (optional) [string]\n+            the ssl digest type to be used in signing the certificate signing request(csr)\n+        :param ACME_REQUEST_TIMEOUT:         (optional) [integer]\n+            the max time that the client will wait for a network call to complete.\n+        :param ACME_AUTH_STATUS_WAIT_PERIOD: (optional) [integer]\n+            the interval between two consecutive client polls on the acme server to check on authorization status\n+        :param ACME_AUTH_STATUS_MAX_CHECKS:  (optional) [integer]\n+            the max number of times the client will poll the acme server to check on authorization status\n+        :param ACME_DIRECTORY_URL:           (optional) [string]\n+            the url of the acme servers' directory endpoint\n+        \"\"\"\n+\n+        self.logger = get_logger(__name__).bind(\n+            sewer_ver=sewer_version.__version__)\n+\n+        self.domain_name = domain_name\n+        self.dns_class = dns_class\n+        self.domain_alt_names = domain_alt_names\n+        self.all_domain_names = copy.copy(self.domain_alt_names)\n+        self.all_domain_names.insert(0, self.domain_name)\n+        self.contact_email = contact_email\n+        self.bits = bits\n+        self.digest = digest\n+        self.ACME_REQUEST_TIMEOUT = ACME_REQUEST_TIMEOUT\n+        self.ACME_AUTH_STATUS_WAIT_PERIOD = ACME_AUTH_STATUS_WAIT_PERIOD\n+        self.ACME_AUTH_STATUS_MAX_CHECKS = ACME_AUTH_STATUS_MAX_CHECKS\n+        self.ACME_DIRECTORY_URL = ACME_DIRECTORY_URL\n+\n+        self.User_Agent = self.get_user_agent()\n+        acme_endpoints = self.get_acme_endpoints().json()\n+        self.ACME_GET_NONCE_URL = acme_endpoints['newNonce']\n+        self.ACME_TOS_URL = acme_endpoints['meta']['termsOfService']\n+        self.ACME_KEY_CHANGE_URL = acme_endpoints['keyChange']\n+        self.ACME_NEW_ACCOUNT_URL = acme_endpoints['newAccount']\n+        self.ACME_NEW_ORDER_URL = acme_endpoints['newOrder']\n+        self.ACME_REVOKE_CERT_URL = acme_endpoints['revokeCert']\n+\n+        # unique account identifier\n+        # https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-6.2\n+        self.kid = None\n+\n+        self.certificate_key = self.create_certificate_key()\n+        self.csr = self.create_csr()\n+\n+        if not account_key:\n+            self.account_key = self.create_account_key()\n+            self.PRIOR_REGISTERED = False\n+        else:\n+            self.account_key = account_key\n+            self.PRIOR_REGISTERED = True\n+\n+        self.logger = self.logger.bind(\n+            sewer_ver=sewer_version.__version__,\n+            domain_names=self.all_domain_names,\n+            acme_server=self.ACME_DIRECTORY_URL[:20] + '...')\n+\n+    def log_response(self, response):\n+        \"\"\"\n+        renders response as json or as a string\n+        \"\"\"\n+        # TODO: use this to handle all response logs.\n+        try:\n+            log_body = response.json()\n+        except ValueError:\n+            log_body = response.content[:30]\n+        return log_body\n+\n+    def get_user_agent(self):\n+        return \"python-requests/{requests_version} ({system}: {machine}) sewer {sewer_version} ({sewer_url})\".format(\n+            requests_version=requests.__version__,\n+            system=platform.system(),\n+            machine=platform.machine(),\n+            sewer_version=sewer_version.__version__,\n+            sewer_url=sewer_version.__url__)\n+\n+    def get_acme_endpoints(self):\n+        self.logger.info('get_acme_endpoints')\n+        headers = {'User-Agent': self.User_Agent}\n+        get_acme_endpoints = requests.get(\n+            self.ACME_DIRECTORY_URL,\n+            timeout=self.ACME_REQUEST_TIMEOUT,\n+            headers=headers)\n+        self.logger.info(\n+            'get_acme_endpoints_response',\n+            status_code=get_acme_endpoints.status_code)\n+        if get_acme_endpoints.status_code not in [200, 201]:\n+            raise ValueError(\n+                \"Error while getting Acme endpoints: status_code={status_code} response={response}\". format(\n+                    status_code=get_acme_endpoints.status_code,\n+                    response=self.log_response(get_acme_endpoints)))\n+        return get_acme_endpoints\n+\n+    def create_account_key(self):\n+        self.logger.info('create_account_key')\n+        return self.create_key()\n+\n+    def create_certificate_key(self):\n+        self.logger.info('create_certificate_key')\n+        return self.create_key()\n+\n+    def create_key(self, key_type=OpenSSL.crypto.TYPE_RSA):\n+        key = OpenSSL.crypto.PKey()\n+        key.generate_key(key_type, self.bits)\n+        private_key = OpenSSL.crypto.dump_privatekey(\n+            OpenSSL.crypto.FILETYPE_PEM, key)\n+        return private_key\n+\n+    def create_csr(self):\n+        \"\"\"\n+        https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-7.4\n+        The CSR is sent in the base64url-encoded version of the DER format. (NB: this\n+        field uses base64url, and does not include headers, it is different from PEM.)\n+        \"\"\"\n+        self.logger.info('create_csr')\n+        X509Req = OpenSSL.crypto.X509Req()\n+        X509Req.get_subject().CN = self.domain_name\n+\n+        if self.domain_alt_names:\n+            SAN = 'DNS:{0}, '.format(self.domain_name).encode('utf8') + \\\n+                  ', '.join('DNS:' + i for i in self.domain_alt_names).encode('utf8')\n+        else:\n+            SAN = 'DNS:{0}'.format(self.domain_name).encode('utf8')\n+\n+        X509Req.add_extensions([\n+            OpenSSL.crypto.X509Extension(\n+                'subjectAltName'.encode('utf8'), critical=False, value=SAN)\n+        ])\n+        pk = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,\n+                                            self.certificate_key)\n+        X509Req.set_pubkey(pk)\n+        X509Req.set_version(2)\n+        X509Req.sign(pk, self.digest)\n+        return OpenSSL.crypto.dump_certificate_request(\n+            OpenSSL.crypto.FILETYPE_ASN1, X509Req)\n+\n+    def calculate_safe_base64(self, un_encoded_data):\n+        \"\"\"\n+        takes in a string or bytes\n+        returns a string\n+        \"\"\"\n+        if isinstance(un_encoded_data, str):\n+            un_encoded_data = un_encoded_data.encode('utf8')\n+        r = base64.urlsafe_b64encode(un_encoded_data).rstrip(b'=')\n+        return r.decode('utf8')\n+\n+    def sign_message(self, message):\n+        self.logger.info('sign_message')\n+        pk = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,\n+                                            self.account_key)\n+        return OpenSSL.crypto.sign(pk, message.encode('utf8'), self.digest)\n+\n+    def get_nonce(self):\n+        \"\"\"\n+        https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-6.4\n+        Each request to an ACME server must include a fresh unused nonce\n+        in order to protect against replay attacks.\n+        \"\"\"\n+        self.logger.info('get_nonce')\n+        headers = {'User-Agent': self.User_Agent}\n+        response = requests.get(\n+            self.ACME_GET_NONCE_URL,\n+            timeout=self.ACME_REQUEST_TIMEOUT,\n+            headers=headers)\n+        nonce = response.headers['Replay-Nonce']\n+        return nonce\n+\n+    def get_acme_header(self, url):\n+        \"\"\"\n+        https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-6.2\n+        The JWS Protected Header MUST include the following fields:\n+        - \"alg\" (Algorithm)\n+        - \"jwk\" (JSON Web Key, only for requests to new-account and revoke-cert resources)\n+        - \"kid\" (Key ID, for all other requests). gotten from self.ACME_NEW_ACCOUNT_URL\n+        - \"nonce\". gotten from self.ACME_GET_NONCE_URL\n+        - \"url\"\n+        \"\"\"\n+        self.logger.info('get_acme_header')\n+        header = {\"alg\": \"RS256\", \"nonce\": self.get_nonce(), \"url\": url}\n+\n+        if url in [\n+                self.ACME_NEW_ACCOUNT_URL,\n+                self.ACME_REVOKE_CERT_URL,\n+                \"GET_THUMBPRINT\"]:\n+            private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(\n+                self.account_key,\n+                password=None,\n+                backend=cryptography.hazmat.backends.default_backend())\n+            public_key_public_numbers = private_key.public_key().public_numbers()\n+            # private key public exponent in hex format\n+            exponent = \"{0:x}\".format(public_key_public_numbers.e)\n+            exponent = \"0{0}\".format(exponent) if len(\n+                exponent) % 2 else exponent\n+            # private key modulus in hex format\n+            modulus = \"{0:x}\".format(public_key_public_numbers.n)\n+            jwk = {\n+                \"kty\": \"RSA\", \"e\": self.calculate_safe_base64(\n+                    binascii.unhexlify(exponent)), \"n\": self.calculate_safe_base64(\n+                    binascii.unhexlify(modulus))}\n+            header[\"jwk\"] = jwk\n+        else:\n+            header[\"kid\"] = self.kid\n+\n+        return header\n+\n+    def apply_for_cert_issuance(self):\n+        \"\"\"\n+        https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-7.4\n+        The order object returned by the server represents a promise that if\n+        the client fulfills the server's requirements before the \"expires\"\n+        time, then the server will be willing to finalize the order upon\n+        request and issue the requested certificate.  In the order object,\n+        any authorization referenced in the \"authorizations\" array whose\n+        status is \"pending\" represents an authorization transaction that the\n+        client must complete before the server will issue the certificate.\n+\n+        Once the client believes it has fulfilled the server's requirements,\n+        it should send a POST request to the order resource's finalize URL.\n+        The POST body MUST include a CSR:\n+\n+        The date values seem to be ignored by LetsEncrypt although they are\n+        in the ACME draft spec; https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-7.4\n+        \"\"\"\n+        self.logger.info('apply_for_cert_issuance')\n+        # TODO: factor in self.all_domain_names instead of just\n+        # self.domain_name\n+        payload = {\"identifiers\": [\n+            {\"type\": \"dns\", \"value\": self.domain_name}], }\n+        url = self.ACME_NEW_ORDER_URL\n+        apply_for_cert_issuance_response = self.make_signed_acme_request(\n+            url=url,\n+            payload=payload)\n+        self.logger.info(\n+            'apply_for_cert_issuance_response',\n+            status_code=apply_for_cert_issuance_response.status_code,\n+            response=self.log_response(apply_for_cert_issuance_response))\n+\n+        if apply_for_cert_issuance_response.status_code != 201:\n+            raise ValueError(\n+                \"Error applying for certificate issuance: status_code={status_code} response={response}\". format(\n+                    status_code=apply_for_cert_issuance_response.status_code,\n+                    response=self.log_response(apply_for_cert_issuance_response)))\n+\n+        apply_for_cert_issuance_response_json = apply_for_cert_issuance_response.json()\n+        # list\n+        authorizations = apply_for_cert_issuance_response_json[\"authorizations\"]\n+        authorization_url = authorizations[0]\n+        finalize_url = apply_for_cert_issuance_response_json[\"finalize\"]\n+\n+        return authorization_url, finalize_url\n+\n+    def send_csr(self, finalize_url):\n+        \"\"\"\n+        https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-7.4\n+        Once the client believes it has fulfilled the server's requirements,\n+        it should send a POST request(include a CSR) to the order resource's finalize URL.\n+        A request to finalize an order will result in error if the order indicated does not have status \"pending\",\n+        if the CSR and order identifiers differ, or if the account is not authorized for the identifiers indicated in the CSR.\n+        The CSR is sent in the base64url-encoded version of the DER format(OpenSSL.crypto.FILETYPE_ASN1)\n+\n+        A valid request to finalize an order will return the order to be finalized.\n+        The client should begin polling the order by sending a\n+        GET request to the order resource to obtain its current state.\n+        \"\"\"\n+        self.logger.info('send_csr')\n+        payload = {\"csr\": self.calculate_safe_base64(self.csr)}\n+        send_csr_response = self.make_signed_acme_request(\n+            url=finalize_url, payload=payload)\n+        self.logger.info(\n+            'send_csr_response',\n+            status_code=send_csr_response.status_code,\n+            response=self.log_response(send_csr_response))\n+\n+        if send_csr_response.status_code not in [200, 201]:\n+            raise ValueError(\n+                \"Error sending csr: status_code={status_code} response={response}\". format(\n+                    status_code=send_csr_response.status_code,\n+                    response=self.log_response(send_csr_response)))\n+        send_csr_response_json = send_csr_response.json()\n+        certificate_url = send_csr_response_json[\"certificate\"]\n+        return certificate_url\n+\n+    def stringfy_items(self, payload):","path":"sewer/client.py","position":363,"original_position":363,"commit_id":"a8b3fed76c90d573ea4ee1e20e201d7edbbcea38","original_commit_id":"a8b3fed76c90d573ea4ee1e20e201d7edbbcea38","user":{"login":"codacy-bot","id":19940114,"avatar_url":"https://avatars1.githubusercontent.com/u/19940114?v=4","gravatar_id":"","url":"https://api.github.com/users/codacy-bot","html_url":"https://github.com/codacy-bot","followers_url":"https://api.github.com/users/codacy-bot/followers","following_url":"https://api.github.com/users/codacy-bot/following{/other_user}","gists_url":"https://api.github.com/users/codacy-bot/gists{/gist_id}","starred_url":"https://api.github.com/users/codacy-bot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codacy-bot/subscriptions","organizations_url":"https://api.github.com/users/codacy-bot/orgs","repos_url":"https://api.github.com/users/codacy-bot/repos","events_url":"https://api.github.com/users/codacy-bot/events{/privacy}","received_events_url":"https://api.github.com/users/codacy-bot/received_events","type":"User","site_admin":false},"body":"![Codacy](https://app.codacy.com/assets/images/favicon.png) Issue found: [Method could be a function](https://app.codacy.com/app/komuW/sewer/pullRequest?prid=1379897)","created_at":"2018-03-04T08:49:47Z","updated_at":"2018-03-04T08:49:47Z","html_url":"https://github.com/komuw/sewer/pull/73#discussion_r172041360","pull_request_url":"https://api.github.com/repos/komuw/sewer/pulls/73","author_association":"NONE","_links":{"self":{"href":"https://api.github.com/repos/komuw/sewer/pulls/comments/172041360"},"html":{"href":"https://github.com/komuw/sewer/pull/73#discussion_r172041360"},"pull_request":{"href":"https://api.github.com/repos/komuw/sewer/pulls/73"}}},"pull_request":{"url":"https://api.github.com/repos/komuw/sewer/pulls/73","id":171667378,"html_url":"https://github.com/komuw/sewer/pull/73","diff_url":"https://github.com/komuw/sewer/pull/73.diff","patch_url":"https://github.com/komuw/sewer/pull/73.patch","issue_url":"https://api.github.com/repos/komuw/sewer/issues/73","number":73,"state":"open","locked":false,"title":"Acmev2","user":{"login":"komuw","id":5163857,"avatar_url":"https://avatars2.githubusercontent.com/u/5163857?v=4","gravatar_id":"","url":"https://api.github.com/users/komuw","html_url":"https://github.com/komuw","followers_url":"https://api.github.com/users/komuw/followers","following_url":"https://api.github.com/users/komuw/following{/other_user}","gists_url":"https://api.github.com/users/komuw/gists{/gist_id}","starred_url":"https://api.github.com/users/komuw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/komuw/subscriptions","organizations_url":"https://api.github.com/users/komuw/orgs","repos_url":"https://api.github.com/users/komuw/repos","events_url":"https://api.github.com/users/komuw/events{/privacy}","received_events_url":"https://api.github.com/users/komuw/received_events","type":"User","site_admin":false},"body":"Thank you for contributing to sewer.                    \r\nEvery contribution to sewer is important to us.                       \r\nYou may not know it, but you have just contributed to making the world a more safer and secure place.                         \r\n\r\nContributor offers to license certain software (a “Contribution” or multiple\r\n“Contributions”) to sewer, and sewer agrees to accept said Contributions,\r\nunder the terms of the MIT License.\r\nContributor understands and agrees that sewer shall have the irrevocable and perpetual right to make\r\nand distribute copies of any Contribution, as well as to create and distribute collective works and\r\nderivative works of any Contribution, under the MIT License.\r\n\r\n\r\nNow,                   \r\n\r\n## What(What have you changed?)\r\n- support acme version 2\r\n\r\n## Why(Why did you change it?)\r\n- its the future\r\n- fix; https://github.com/komuw/sewer/issues/68\r\n- fix; https://github.com/komuw/sewer/issues/69\r\n\r\n# TODO:\r\n- on merge send a PR to https://github.com/komuw/website/blob/master/content/docs/client-options.md \r\nto show that sewer full integrates with acme v2 production endpoints","created_at":"2018-02-27T13:49:27Z","updated_at":"2018-03-04T08:49:47Z","closed_at":null,"merged_at":null,"merge_commit_sha":"5adc5bfd5a642e6b14f58b95df887f774fd5ae8d","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/komuw/sewer/pulls/73/commits","review_comments_url":"https://api.github.com/repos/komuw/sewer/pulls/73/comments","review_comment_url":"https://api.github.com/repos/komuw/sewer/pulls/comments{/number}","comments_url":"https://api.github.com/repos/komuw/sewer/issues/73/comments","statuses_url":"https://api.github.com/repos/komuw/sewer/statuses/a8b3fed76c90d573ea4ee1e20e201d7edbbcea38","head":{"label":"komuw:acmev2","ref":"acmev2","sha":"a8b3fed76c90d573ea4ee1e20e201d7edbbcea38","user":{"login":"komuw","id":5163857,"avatar_url":"https://avatars2.githubusercontent.com/u/5163857?v=4","gravatar_id":"","url":"https://api.github.com/users/komuw","html_url":"https://github.com/komuw","followers_url":"https://api.github.com/users/komuw/followers","following_url":"https://api.github.com/users/komuw/following{/other_user}","gists_url":"https://api.github.com/users/komuw/gists{/gist_id}","starred_url":"https://api.github.com/users/komuw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/komuw/subscriptions","organizations_url":"https://api.github.com/users/komuw/orgs","repos_url":"https://api.github.com/users/komuw/repos","events_url":"https://api.github.com/users/komuw/events{/privacy}","received_events_url":"https://api.github.com/users/komuw/received_events","type":"User","site_admin":false},"repo":{"id":96523010,"name":"sewer","full_name":"komuw/sewer","owner":{"login":"komuw","id":5163857,"avatar_url":"https://avatars2.githubusercontent.com/u/5163857?v=4","gravatar_id":"","url":"https://api.github.com/users/komuw","html_url":"https://github.com/komuw","followers_url":"https://api.github.com/users/komuw/followers","following_url":"https://api.github.com/users/komuw/following{/other_user}","gists_url":"https://api.github.com/users/komuw/gists{/gist_id}","starred_url":"https://api.github.com/users/komuw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/komuw/subscriptions","organizations_url":"https://api.github.com/users/komuw/orgs","repos_url":"https://api.github.com/users/komuw/repos","events_url":"https://api.github.com/users/komuw/events{/privacy}","received_events_url":"https://api.github.com/users/komuw/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/komuw/sewer","description":"Let's Encrypt(ACME) client. Programmatic/library","fork":false,"url":"https://api.github.com/repos/komuw/sewer","forks_url":"https://api.github.com/repos/komuw/sewer/forks","keys_url":"https://api.github.com/repos/komuw/sewer/keys{/key_id}","collaborators_url":"https://api.github.com/repos/komuw/sewer/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/komuw/sewer/teams","hooks_url":"https://api.github.com/repos/komuw/sewer/hooks","issue_events_url":"https://api.github.com/repos/komuw/sewer/issues/events{/number}","events_url":"https://api.github.com/repos/komuw/sewer/events","assignees_url":"https://api.github.com/repos/komuw/sewer/assignees{/user}","branches_url":"https://api.github.com/repos/komuw/sewer/branches{/branch}","tags_url":"https://api.github.com/repos/komuw/sewer/tags","blobs_url":"https://api.github.com/repos/komuw/sewer/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/komuw/sewer/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/komuw/sewer/git/refs{/sha}","trees_url":"https://api.github.com/repos/komuw/sewer/git/trees{/sha}","statuses_url":"https://api.github.com/repos/komuw/sewer/statuses/{sha}","languages_url":"https://api.github.com/repos/komuw/sewer/languages","stargazers_url":"https://api.github.com/repos/komuw/sewer/stargazers","contributors_url":"https://api.github.com/repos/komuw/sewer/contributors","subscribers_url":"https://api.github.com/repos/komuw/sewer/subscribers","subscription_url":"https://api.github.com/repos/komuw/sewer/subscription","commits_url":"https://api.github.com/repos/komuw/sewer/commits{/sha}","git_commits_url":"https://api.github.com/repos/komuw/sewer/git/commits{/sha}","comments_url":"https://api.github.com/repos/komuw/sewer/comments{/number}","issue_comment_url":"https://api.github.com/repos/komuw/sewer/issues/comments{/number}","contents_url":"https://api.github.com/repos/komuw/sewer/contents/{+path}","compare_url":"https://api.github.com/repos/komuw/sewer/compare/{base}...{head}","merges_url":"https://api.github.com/repos/komuw/sewer/merges","archive_url":"https://api.github.com/repos/komuw/sewer/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/komuw/sewer/downloads","issues_url":"https://api.github.com/repos/komuw/sewer/issues{/number}","pulls_url":"https://api.github.com/repos/komuw/sewer/pulls{/number}","milestones_url":"https://api.github.com/repos/komuw/sewer/milestones{/number}","notifications_url":"https://api.github.com/repos/komuw/sewer/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/komuw/sewer/labels{/name}","releases_url":"https://api.github.com/repos/komuw/sewer/releases{/id}","deployments_url":"https://api.github.com/repos/komuw/sewer/deployments","created_at":"2017-07-07T09:30:40Z","updated_at":"2018-01-25T04:44:03Z","pushed_at":"2018-03-04T08:47:56Z","git_url":"git://github.com/komuw/sewer.git","ssh_url":"git@github.com:komuw/sewer.git","clone_url":"https://github.com/komuw/sewer.git","svn_url":"https://github.com/komuw/sewer","homepage":null,"size":118,"stargazers_count":8,"watchers_count":8,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"archived":false,"open_issues_count":19,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit"},"forks":4,"open_issues":19,"watchers":8,"default_branch":"master"}},"base":{"label":"komuw:master","ref":"master","sha":"0c7c1454625f293f60055a4a891bab24d7fd2203","user":{"login":"komuw","id":5163857,"avatar_url":"https://avatars2.githubusercontent.com/u/5163857?v=4","gravatar_id":"","url":"https://api.github.com/users/komuw","html_url":"https://github.com/komuw","followers_url":"https://api.github.com/users/komuw/followers","following_url":"https://api.github.com/users/komuw/following{/other_user}","gists_url":"https://api.github.com/users/komuw/gists{/gist_id}","starred_url":"https://api.github.com/users/komuw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/komuw/subscriptions","organizations_url":"https://api.github.com/users/komuw/orgs","repos_url":"https://api.github.com/users/komuw/repos","events_url":"https://api.github.com/users/komuw/events{/privacy}","received_events_url":"https://api.github.com/users/komuw/received_events","type":"User","site_admin":false},"repo":{"id":96523010,"name":"sewer","full_name":"komuw/sewer","owner":{"login":"komuw","id":5163857,"avatar_url":"https://avatars2.githubusercontent.com/u/5163857?v=4","gravatar_id":"","url":"https://api.github.com/users/komuw","html_url":"https://github.com/komuw","followers_url":"https://api.github.com/users/komuw/followers","following_url":"https://api.github.com/users/komuw/following{/other_user}","gists_url":"https://api.github.com/users/komuw/gists{/gist_id}","starred_url":"https://api.github.com/users/komuw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/komuw/subscriptions","organizations_url":"https://api.github.com/users/komuw/orgs","repos_url":"https://api.github.com/users/komuw/repos","events_url":"https://api.github.com/users/komuw/events{/privacy}","received_events_url":"https://api.github.com/users/komuw/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/komuw/sewer","description":"Let's Encrypt(ACME) client. Programmatic/library","fork":false,"url":"https://api.github.com/repos/komuw/sewer","forks_url":"https://api.github.com/repos/komuw/sewer/forks","keys_url":"https://api.github.com/repos/komuw/sewer/keys{/key_id}","collaborators_url":"https://api.github.com/repos/komuw/sewer/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/komuw/sewer/teams","hooks_url":"https://api.github.com/repos/komuw/sewer/hooks","issue_events_url":"https://api.github.com/repos/komuw/sewer/issues/events{/number}","events_url":"https://api.github.com/repos/komuw/sewer/events","assignees_url":"https://api.github.com/repos/komuw/sewer/assignees{/user}","branches_url":"https://api.github.com/repos/komuw/sewer/branches{/branch}","tags_url":"https://api.github.com/repos/komuw/sewer/tags","blobs_url":"https://api.github.com/repos/komuw/sewer/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/komuw/sewer/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/komuw/sewer/git/refs{/sha}","trees_url":"https://api.github.com/repos/komuw/sewer/git/trees{/sha}","statuses_url":"https://api.github.com/repos/komuw/sewer/statuses/{sha}","languages_url":"https://api.github.com/repos/komuw/sewer/languages","stargazers_url":"https://api.github.com/repos/komuw/sewer/stargazers","contributors_url":"https://api.github.com/repos/komuw/sewer/contributors","subscribers_url":"https://api.github.com/repos/komuw/sewer/subscribers","subscription_url":"https://api.github.com/repos/komuw/sewer/subscription","commits_url":"https://api.github.com/repos/komuw/sewer/commits{/sha}","git_commits_url":"https://api.github.com/repos/komuw/sewer/git/commits{/sha}","comments_url":"https://api.github.com/repos/komuw/sewer/comments{/number}","issue_comment_url":"https://api.github.com/repos/komuw/sewer/issues/comments{/number}","contents_url":"https://api.github.com/repos/komuw/sewer/contents/{+path}","compare_url":"https://api.github.com/repos/komuw/sewer/compare/{base}...{head}","merges_url":"https://api.github.com/repos/komuw/sewer/merges","archive_url":"https://api.github.com/repos/komuw/sewer/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/komuw/sewer/downloads","issues_url":"https://api.github.com/repos/komuw/sewer/issues{/number}","pulls_url":"https://api.github.com/repos/komuw/sewer/pulls{/number}","milestones_url":"https://api.github.com/repos/komuw/sewer/milestones{/number}","notifications_url":"https://api.github.com/repos/komuw/sewer/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/komuw/sewer/labels{/name}","releases_url":"https://api.github.com/repos/komuw/sewer/releases{/id}","deployments_url":"https://api.github.com/repos/komuw/sewer/deployments","created_at":"2017-07-07T09:30:40Z","updated_at":"2018-01-25T04:44:03Z","pushed_at":"2018-03-04T08:47:56Z","git_url":"git://github.com/komuw/sewer.git","ssh_url":"git@github.com:komuw/sewer.git","clone_url":"https://github.com/komuw/sewer.git","svn_url":"https://github.com/komuw/sewer","homepage":null,"size":118,"stargazers_count":8,"watchers_count":8,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"archived":false,"open_issues_count":19,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit"},"forks":4,"open_issues":19,"watchers":8,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/komuw/sewer/pulls/73"},"html":{"href":"https://github.com/komuw/sewer/pull/73"},"issue":{"href":"https://api.github.com/repos/komuw/sewer/issues/73"},"comments":{"href":"https://api.github.com/repos/komuw/sewer/issues/73/comments"},"review_comments":{"href":"https://api.github.com/repos/komuw/sewer/pulls/73/comments"},"review_comment":{"href":"https://api.github.com/repos/komuw/sewer/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/komuw/sewer/pulls/73/commits"},"statuses":{"href":"https://api.github.com/repos/komuw/sewer/statuses/a8b3fed76c90d573ea4ee1e20e201d7edbbcea38"}},"author_association":"OWNER"}}
{"actor":{"display_login":"codacy-bot"}}