Beispiel #1
0
    def delete_log(self, log_type, index=0):
        if index:
            print(
                snakesay(
                    "Deleting old (archive number {index}) {type} log file for {domain} via API"
                    .format(index=index, type=log_type, domain=self.domain)))
        else:
            print(
                snakesay(
                    "Deleting current {type} log file for {domain} via API".
                    format(type=log_type, domain=self.domain)))

        if index == 1:
            url = get_api_endpoint().format(
                username=getpass.getuser(),
                flavor="files") + "path/var/log/{domain}.{type}.log.1/".format(
                    domain=self.domain, type=log_type)
        elif index > 1:
            url = get_api_endpoint().format(
                username=getpass.getuser(), flavor="files"
            ) + "path/var/log/{domain}.{type}.log.{index}.gz/".format(
                domain=self.domain, type=log_type, index=index)
        else:
            url = get_api_endpoint().format(
                username=getpass.getuser(),
                flavor="files") + "path/var/log/{domain}.{type}.log/".format(
                    domain=self.domain, type=log_type)
        response = call_api(url, "delete")
        if not response.ok:
            raise Exception(
                "DELETE log file via API failed, got {response}:{response_text}"
                .format(response=response, response_text=response.text))
Beispiel #2
0
    def test_does_patch_to_update_virtualenv_path_and_source_directory(
            self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        expected_patch_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(
            responses.POST,
            expected_post_url,
            status=201,
            body=json.dumps({"status": "OK"}),
        )
        api_responses.add(responses.PATCH, expected_patch_url, status=200)

        Webapp("mydomain.com").create("3.7",
                                      "/virtualenv/path",
                                      "/project/path",
                                      nuke=False)

        patch = api_responses.calls[1]
        assert patch.request.url == expected_patch_url
        assert patch.request.body == urlencode({
            "virtualenv_path":
            "/virtualenv/path",
            "source_directory":
            "/project/path"
        })
        assert patch.request.headers["Authorization"] == f"Token {api_token}"
Beispiel #3
0
    def test_raises_if_patch_does_not_20x(self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        expected_patch_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(
            responses.POST,
            expected_post_url,
            status=201,
            body=json.dumps({"status": "OK"}),
        )
        api_responses.add(
            responses.PATCH,
            expected_patch_url,
            status=400,
            json={"message": "an error"},
        )

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").create("3.7",
                                          "/virtualenv/path",
                                          "/project/path",
                                          nuke=False)

        assert (
            "PATCH to set virtualenv path and source directory via API failed"
            in str(e.value))
        assert "an error" in str(e.value)
Beispiel #4
0
    def test_does_post_to_create_webapp(self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        expected_patch_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(
            responses.POST,
            expected_post_url,
            status=201,
            body=json.dumps({"status": "OK"}),
        )
        api_responses.add(responses.PATCH, expected_patch_url, status=200)

        Webapp("mydomain.com").create("3.7",
                                      "/virtualenv/path",
                                      "/project/path",
                                      nuke=False)

        post = api_responses.calls[0]
        assert post.request.url == expected_post_url
        assert post.request.body == urlencode({
            "domain_name":
            "mydomain.com",
            "python_version":
            PYTHON_VERSIONS["3.7"]
        })
        assert post.request.headers["Authorization"] == f"Token {api_token}"
Beispiel #5
0
 def create(self, python_version, virtualenv_path, project_path, nuke):
     print(snakesay("Creating web app via API"))
     if nuke:
         webapp_url = get_api_endpoint().format(
             username=getpass.getuser(),
             flavor="webapps") + self.domain + "/"
         call_api(webapp_url, "delete")
     post_url = get_api_endpoint().format(username=getpass.getuser(),
                                          flavor="webapps")
     patch_url = post_url + self.domain + "/"
     response = call_api(post_url,
                         "post",
                         data={
                             "domain_name": self.domain,
                             "python_version":
                             PYTHON_VERSIONS[python_version]
                         })
     if not response.ok or response.json().get("status") == "ERROR":
         raise Exception(
             "POST to create webapp via API failed, got {response}:{response_text}"
             .format(response=response, response_text=response.text))
     response = call_api(patch_url,
                         "patch",
                         data={
                             "virtualenv_path": virtualenv_path,
                             "source_directory": project_path
                         })
     if not response.ok:
         raise Exception(
             "PATCH to set virtualenv path and source directory via API failed,"
             "got {response}:{response_text}".format(
                 response=response, response_text=response.text))
Beispiel #6
0
 def get_log_info(self):
     url = get_api_endpoint().format(
         username=getpass.getuser(),
         flavor="files") + "tree/?path=/var/log/"
     response = call_api(url, "get")
     if not response.ok:
         raise Exception(
             "GET log files info via API failed, got {response}:{response_text}"
             .format(response=response, response_text=response.text))
     file_list = response.json()
     log_types = ["access", "error", "server"]
     logs = {"access": [], "error": [], "server": []}
     log_prefix = "/var/log/{domain}.".format(domain=self.domain)
     for file_name in file_list:
         if type(file_name) == str and file_name.startswith(log_prefix):
             log = file_name[len(log_prefix):].split(".")
             if log[0] in log_types:
                 log_type = log[0]
                 if log[-1] == "log":
                     log_index = 0
                 elif log[-1] == "1":
                     log_index = 1
                 elif log[-1] == "gz":
                     log_index = int(log[-2])
                 else:
                     continue
                 logs[log_type].append(log_index)
     return logs
Beispiel #7
0
    def test_does_two_posts_to_static_files_endpoint(self, api_token,
                                                     api_responses):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/static_files/")
        api_responses.add(responses.POST, expected_url, status=201)
        api_responses.add(responses.POST, expected_url, status=201)

        Webapp("mydomain.com").add_default_static_files_mappings(
            "/project/path")

        post1 = api_responses.calls[0]
        assert post1.request.url == expected_url
        assert post1.request.headers["content-type"] == "application/json"
        assert post1.request.headers["Authorization"] == f"Token {api_token}"
        assert json.loads(post1.request.body.decode("utf8")) == {
            "url": "/static/",
            "path": "/project/path/static",
        }
        post2 = api_responses.calls[1]
        assert post2.request.url == expected_url
        assert post2.request.headers["content-type"] == "application/json"
        assert post2.request.headers["Authorization"] == f"Token {api_token}"
        assert json.loads(post2.request.body.decode("utf8")) == {
            "url": "/media/",
            "path": "/project/path/media",
        }
class TestFiles:
    username = getpass.getuser()
    base_url = get_api_endpoint().format(username=username, flavor="files")
    home_dir_path = f"/home/{username}"
    default_home_dir_files = {
        ".bashrc": {
            "type": "file",
            "url": f"{base_url}path{home_dir_path}/.bashrc"
        },
        ".gitconfig": {
            "type": "file",
            "url": f"{base_url}path{home_dir_path}/.gitconfig"
        },
        ".local": {
            "type": "directory",
            "url": f"{base_url}path{home_dir_path}/.local"
        },
        ".profile": {
            "type": "file",
            "url": f"{base_url}path{home_dir_path}/.profile"
        },
        "README.txt": {
            "type": "file",
            "url": f"{base_url}path{home_dir_path}/README.txt"
        },
    }
    readme_contents = (
        b"# vim: set ft=rst:\n\nSee https://help.pythonanywhere.com/ "
        b'(or click the "Help" link at the top\nright) '
        b"for help on how to use PythonAnywhere, including tips on copying and\n"
        b"pasting from consoles, and writing your own web applications.\n")
Beispiel #9
0
    def sanity_checks(self, nuke):
        print(snakesay("Running API sanity checks"))
        token = os.environ.get("API_TOKEN")
        if not token:
            raise SanityException(
                dedent(
                    """
                Could not find your API token.
                You may need to create it on the Accounts page?
                You will also need to close this console and open a new one once you've done that.
                """
                )
            )

        if nuke:
            return

        url = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + self.domain + "/"
        response = call_api(url, "get")
        if response.status_code == 200:
            raise SanityException(
                "You already have a webapp for {domain}.\n\nUse the --nuke option if you want to replace it.".format(
                    domain=self.domain
                )
            )
Beispiel #10
0
    def test_returns_json_from_server_having_parsed_expiry_with_timezone_offset_and_separators(
            self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/ssl/")
        api_responses.add(
            responses.GET,
            expected_url,
            status=200,
            body=json.dumps({
                "not_after":
                "2018-08-24T17:16:23+00:00",
                "issuer_name":
                "PythonAnywhere test CA",
                "subject_name":
                "www.mycoolsite.com",
                "subject_alternate_names":
                ["www.mycoolsite.com", "mycoolsite.com"],
            }),
        )

        assert Webapp("mydomain.com").get_ssl_info() == {
            "not_after": datetime(2018, 8, 24, 17, 16, 23, tzinfo=tzutc()),
            "issuer_name": "PythonAnywhere test CA",
            "subject_name": "www.mycoolsite.com",
            "subject_alternate_names":
            ["www.mycoolsite.com", "mycoolsite.com"],
        }

        get = api_responses.calls[0]
        assert get.request.method == "GET"
        assert get.request.url == expected_url
        assert get.request.headers["Authorization"] == f"Token {api_token}"
Beispiel #11
0
    def test_get_list_of_logs(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="files") +
                        "tree/?path=/var/log/")
        api_responses.add(
            responses.GET,
            expected_url,
            status=200,
            body=json.dumps([
                "/var/log/blah",
                "/var/log/mydomain.com.access.log",
                "/var/log/mydomain.com.access.log.1",
                "/var/log/mydomain.com.access.log.2.gz",
                "/var/log/mydomain.com.error.log",
                "/var/log/mydomain.com.error.log.1",
                "/var/log/mydomain.com.error.log.2.gz",
                "/var/log/mydomain.com.server.log",
                "/var/log/mydomain.com.server.log.1",
                "/var/log/mydomain.com.server.log.2.gz",
            ]),
        )

        logs = Webapp("mydomain.com").get_log_info()

        post = api_responses.calls[0]
        assert post.request.url == expected_url
        assert post.request.headers["Authorization"] == f"Token {api_token}"
        assert logs == {
            "access": [0, 1, 2],
            "error": [0, 1, 2],
            "server": [0, 1, 2]
        }
Beispiel #12
0
    def test_ignores_404_from_delete_call_when_nuking(self, api_responses,
                                                      api_token):
        post_url = get_api_endpoint().format(username=getpass.getuser(),
                                             flavor="webapps")
        webapp_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(responses.DELETE, webapp_url, status=404)
        api_responses.add(responses.POST,
                          post_url,
                          status=201,
                          body=json.dumps({"status": "OK"}))
        api_responses.add(responses.PATCH, webapp_url, status=200)

        Webapp("mydomain.com").create("3.7",
                                      "/virtualenv/path",
                                      "/project/path",
                                      nuke=True)
Beispiel #13
0
    def add_default_static_files_mappings(self, project_path):
        print(snakesay("Adding static files mappings for /static/ and /media/"))

        url = (
            get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + self.domain + "/static_files/"
        )
        call_api(url, "post", json=dict(url="/static/", path=str(Path(project_path) / "static")))
        call_api(url, "post", json=dict(url="/media/", path=str(Path(project_path) / "media")))
Beispiel #14
0
    def test_raises_if_get_does_not_20x(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="files") +
                        "tree/?path=/var/log/")
        api_responses.add(responses.GET, expected_url, status=404, body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").get_log_info()

        assert "GET log files info via API failed" in str(e.value)
        assert "nope" in str(e.value)
Beispiel #15
0
    def test_raises_if_get_does_not_return_200(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/ssl/")
        api_responses.add(responses.GET, expected_url, status=404, body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").get_ssl_info()

        assert "GET SSL details via API failed, got" in str(e.value)
        assert "nope" in str(e.value)
Beispiel #16
0
    def get_ssl_info(self):
        url = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + self.domain + "/ssl/"
        response = call_api(url, "get")
        if not response.ok:
            raise Exception(
                f"GET SSL details via API failed, got {response}:{response.text}"
            )

        result = response.json()
        result["not_after"] = parse(result["not_after"])
        return result
Beispiel #17
0
    def test_does_delete_first_for_nuke_call(self, api_responses, api_token):
        post_url = get_api_endpoint().format(username=getpass.getuser(),
                                             flavor="webapps")
        webapp_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(responses.DELETE, webapp_url, status=200)
        api_responses.add(responses.POST,
                          post_url,
                          status=201,
                          body=json.dumps({"status": "OK"}))
        api_responses.add(responses.PATCH, webapp_url, status=200)

        Webapp("mydomain.com").create("3.7",
                                      "/virtualenv/path",
                                      "/project/path",
                                      nuke=True)

        delete = api_responses.calls[0]
        assert delete.request.method == "DELETE"
        assert delete.request.url == webapp_url
        assert delete.request.headers["Authorization"] == f"Token {api_token}"
Beispiel #18
0
    def test_does_post_to_reload_url(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/reload/")
        api_responses.add(responses.POST, expected_url, status=200)

        Webapp("mydomain.com").reload()

        post = api_responses.calls[0]
        assert post.request.url == expected_url
        assert post.request.body is None
        assert post.request.headers["Authorization"] == f"Token {api_token}"
Beispiel #19
0
    def test_delete_old_access_log(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="files") +
                        "path/var/log/mydomain.com.access.log.1/")
        api_responses.add(responses.DELETE, expected_url, status=200)

        Webapp("mydomain.com").delete_log(log_type="access", index=1)

        post = api_responses.calls[0]
        assert post.request.url == expected_url
        assert post.request.body is None
        assert post.request.headers["Authorization"] == f"Token {api_token}"
Beispiel #20
0
    def test_raises_if_delete_does_not_20x(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="files") +
                        "path/var/log/mydomain.com.access.log/")
        api_responses.add(responses.DELETE,
                          expected_url,
                          status=404,
                          body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").delete_log(log_type="access")

        assert "DELETE log file via API failed" in str(e.value)
        assert "nope" in str(e.value)
Beispiel #21
0
    def test_raises_if_post_does_not_20x(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/ssl/")
        api_responses.add(responses.POST,
                          expected_url,
                          status=404,
                          body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").set_ssl("foo", "bar")

        assert "POST to set SSL details via API failed" in str(e.value)
        assert "nope" in str(e.value)
Beispiel #22
0
 def set_ssl(self, certificate, private_key):
     print(snakesay(f"Setting up SSL for {self.domain} via API"))
     url = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + self.domain + "/ssl/"
     response = call_api(url, "post", json={"cert": certificate, "private_key": private_key})
     if not response.ok:
         raise Exception(
             dedent(
                 """
                 POST to set SSL details via API failed, got {response}:{response_text}
                 If you just created an API token, you need to set the API_TOKEN environment variable or start a
                 new console.  Also you need to have setup a `{domain}` PythonAnywhere webapp for this to work.
                 """
             ).format(response=response, response_text=response.text, domain=self.domain)
         )
Beispiel #23
0
class TestWebappSanityChecks:
    domain = "www.domain.com"
    expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                              flavor="webapps") + domain + "/")

    def test_does_not_complain_if_api_token_exists(self, api_token,
                                                   api_responses):
        webapp = Webapp(self.domain)
        api_responses.add(responses.GET, self.expected_url, status=404)
        webapp.sanity_checks(nuke=False)  # should not raise

    def test_raises_if_no_api_token_exists(self, api_responses, no_api_token):
        webapp = Webapp(self.domain)
        with pytest.raises(SanityException) as e:
            webapp.sanity_checks(nuke=False)
        assert "Could not find your API token" in str(e.value)

    def test_raises_if_webapp_already_exists(self, api_token, api_responses):
        webapp = Webapp(self.domain)
        api_responses.add(
            responses.GET,
            self.expected_url,
            status=200,
            body=json.dumps({
                "id": 1,
                "domain_name": self.domain
            }),
        )

        with pytest.raises(SanityException) as e:
            webapp.sanity_checks(nuke=False)

        assert "You already have a webapp for " + self.domain in str(e.value)
        assert "nuke" in str(e.value)

    def test_does_not_raise_if_no_webapp(self, api_token, api_responses):
        webapp = Webapp(self.domain)
        api_responses.add(responses.GET, self.expected_url, status=404)
        webapp.sanity_checks(nuke=False)  # should not raise

    def test_nuke_option_overrides_all_but_token_check(self, api_token,
                                                       api_responses,
                                                       fake_home,
                                                       virtualenvs_folder):
        webapp = Webapp(self.domain)
        (fake_home / self.domain).mkdir()
        (virtualenvs_folder / self.domain).mkdir()

        webapp.sanity_checks(nuke=True)  # should not raise
Beispiel #24
0
    def test_raises_if_post_does_not_20x_that_is_not_a_cname_error(
            self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/reload/")
        api_responses.add(responses.POST,
                          expected_url,
                          status=404,
                          body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").reload()

        assert "POST to reload webapp via API failed" in str(e.value)
        assert "nope" in str(e.value)
Beispiel #25
0
    def test_raises_if_post_does_not_20x(self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        api_responses.add(responses.POST,
                          expected_post_url,
                          status=500,
                          body="an error")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").create("3.7",
                                          "/virtualenv/path",
                                          "/project/path",
                                          nuke=False)

        assert "POST to create webapp via API failed" in str(e.value)
        assert "an error" in str(e.value)
Beispiel #26
0
    def test_does_post_to_ssl_url(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/ssl/")
        api_responses.add(responses.POST, expected_url, status=200)
        certificate = "certificate data"
        private_key = "private key data"

        Webapp("mydomain.com").set_ssl(certificate, private_key)

        post = api_responses.calls[0]
        assert post.request.url == expected_url
        assert json.loads(post.request.body.decode("utf8")) == {
            "private_key": "private key data",
            "cert": "certificate data",
        }
        assert post.request.headers["Authorization"] == f"Token {api_token}"
Beispiel #27
0
    def test_does_not_raise_if_post_responds_with_a_cname_error(
            self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/reload/")
        api_responses.add(
            responses.POST,
            expected_url,
            status=409,
            json={
                "status": "error",
                "error": "cname_error"
            },
        )

        ## Should not raise
        Webapp("mydomain.com").reload()
Beispiel #28
0
 def reload(self):
     print(snakesay(f"Reloading {self.domain} via API"))
     url = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + self.domain + "/reload/"
     response = call_api(url, "post")
     if not response.ok:
         if response.status_code == 409 and response.json()["error"] == "cname_error":
             print(
                 snakesay(
                     dedent("""
                         Could not find a CNAME for your website.  If you're using an A record,
                         CloudFlare, or some other way of pointing your domain at PythonAnywhere
                         then that should not be a problem.  If you're not, you should double-check
                         your DNS setup.
                     """)
                 )
             )
             return
         raise Exception(
             f"POST to reload webapp via API failed, got {response}:{response.text}"
         )
Beispiel #29
0
    def test_raises_if_post_returns_a_200_with_status_error(
            self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        api_responses.add(
            responses.POST,
            expected_post_url,
            status=200,
            body=json.dumps({
                "status": "ERROR",
                "error_type": "bad",
                "error_message": "bad things happened",
            }),
        )

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").create("3.7",
                                          "/virtualenv/path",
                                          "/project/path",
                                          nuke=False)

        assert "POST to create webapp via API failed" in str(e.value)
        assert "bad things happened" in str(e.value)
class Files:
    """ Interface for PythonAnywhere files API.

    Uses `pythonanywhere.api.base` :method: `get_api_endpoint` to
    create url, which is stored in a class variable `Files.base_url`,
    then calls `call_api` with appropriate arguments to execute files
    action.

    Covers:
    - GET, POST and DELETE for files path endpoint
    - POST, GET and DELETE for files sharing endpoint
    - GET for tree endpoint

    "path" methods:
    - use :method: `Files.path_get` to get contents of file or
    directory from `path`
    - use :method: `Files.path_post` to upload or update file at given
    `dest_path` using contents from `source`
    - use :method: `Files.path_delete` to delete file/directory on on
    given `path`

    "sharing" methods:
    - use :method: `Files.sharing_post` to enable sharing a file from
    `path` (if not shared before) and get a link to it
    - use :method: `Files.sharing_get` to get sharing url for `path`
    - use :method: `Files.sharing_delete` to disable sharing for
    `path`

    "tree" method:
    - use :method: `Files.tree_get` to get list of regular files and
    subdirectories of a directory at `path` (limited to 1000 results)
    """

    base_url = get_api_endpoint().format(username=getpass.getuser(),
                                         flavor="files")
    path_endpoint = urljoin(base_url, "path")
    sharing_endpoint = urljoin(base_url, "sharing/")
    tree_endpoint = urljoin(base_url, "tree/")

    def _error_msg(self, result):
        """TODO: error responses should be unified at the API side """

        if "application/json" in result.headers.get("content-type", ""):
            jsn = result.json()
            msg = jsn.get("detail") or jsn.get("message") or jsn.get(
                "error", "")
            return f": {msg}"
        return ""

    def path_get(self, path):
        """Returns dictionary of directory contents when `path` is an
        absolute path to of an existing directory or file contents if
        `path` is an absolute path to an existing file -- both
        available to the PythonAnywhere user.  Raises when `path` is
        invalid or unavailable."""

        url = f"{self.path_endpoint}{path}"

        result = call_api(url, "GET")

        if result.status_code == 200:
            if "application/json" in result.headers.get("content-type", ""):
                return result.json()
            return result.content

        raise Exception(
            f"GET to fetch contents of {url} failed, got {result}{self._error_msg(result)}"
        )

    def path_post(self, dest_path, content):
        """Uploads contents of `content` to `dest_path` which should be
        a valid absolute path of a file available to a PythonAnywhere
        user.  If `dest_path` contains directories which don't exist
        yet, they will be created.

        Returns 200 if existing file on PythonAnywhere has been
        updated with `source` contents, or 201 if file from
        `dest_path` has been created with those contents."""

        url = f"{self.path_endpoint}{dest_path}"

        result = call_api(url, "POST", files={"content": content})

        if result.ok:
            return result.status_code

        raise Exception(
            f"POST to upload contents to {url} failed, got {result}{self._error_msg(result)}"
        )

    def path_delete(self, path):
        """Deletes the file at specified `path` (if file is a
        directory it will be deleted as well).

        Returns 204 on sucess, raises otherwise."""

        url = f"{self.path_endpoint}{path}"

        result = call_api(url, "DELETE")

        if result.status_code == 204:
            return result.status_code

        raise Exception(
            f"DELETE on {url} failed, got {result}{self._error_msg(result)}")

    def sharing_post(self, path):
        """Starts sharing a file at `path`.

        Returns a tuple with a status code and sharing link on
        success, raises otherwise.  Status code is 201 on success, 200
        if file has been already shared."""

        url = self.sharing_endpoint

        result = call_api(url, "POST", json={"path": path})

        if result.ok:
            return result.status_code, result.json()["url"]

        raise Exception(
            f"POST to {url} to share '{path}' failed, got {result}{self._error_msg(result)}"
        )

    def sharing_get(self, path):
        """Checks sharing status for a `path`.

        Returns url with sharing link if file is shared or an empty
        string otherwise."""

        url = f"{self.sharing_endpoint}?path={path}"

        result = call_api(url, "GET")

        return result.json()["url"] if result.ok else ""

    def sharing_delete(self, path):
        """Stops sharing file at `path`.

        Returns 204 on successful unshare."""

        url = f"{self.sharing_endpoint}?path={path}"

        result = call_api(url, "DELETE")

        return result.status_code

    def tree_get(self, path):
        """Returns list of absolute paths of regular files and
        subdirectories of a directory at `path`.  Result is limited to
        1000 items.

        Raises if `path` does not point to an existing directory."""

        url = f"{self.tree_endpoint}?path={path}"

        result = call_api(url, "GET")

        if result.ok:
            return result.json()

        raise Exception(
            f"GET to {url} failed, got {result}{self._error_msg(result)}")