예제 #1
0
def pullArtifact(artifact, version, user=None, token=None):

    dpl = artifact.deploy
    file = "{filename}_v{version}.{ext}".format(
        filename=artifact.name,
        version=version,
        ext=artifact.file.split(".")[1])
    url = "{url}/{repo}/{path}/{file}".format(url=dpl.url,
                                              repo=dpl.repo,
                                              path=dpl.path,
                                              file=file)

    if (ArtifactoryPath("{url}".format(url=url)).exists()):

        if (user == None):
            user = input("Please provide a valid Artifactory user: "******"Please provide a valid Artifactory token: ")

        print("Pulling {file} from {url}".format(file=file, url=url))
        path = ArtifactoryPath(url, auth=(user, token))
        with path.open() as fd:
            with open(file, "wb") as out:
                out.write(fd.read())
    else:
        print("Artifact Not Found: {url}".format(url=url))
예제 #2
0
def pushArtifact(artifact, version, user=None, token=None, force=False):

    dpl = artifact.deploy
    file = "{filename}_v{version}.{ext}".format(
        filename=artifact.name,
        version=version,
        ext=artifact.file.split(".")[1])
    url = "{url}/{repo}/{path}/{file}".format(url=dpl.url,
                                              repo=dpl.repo,
                                              path=dpl.path,
                                              file=file)

    if (force == False and ArtifactoryPath("{url}".format(url=url)).exists()):
        print(
            "This artifact version has already been pushed. Please bump the version before pushing (skelebot bump) or force push (-f)."
        )
        sys.exit(1)

    if (user == None):
        user = input("Please provide a valid Artifactory user: "******"Please provide a valid Artifactory token: ")

    print("Deploying {file} to {url}".format(file=file, url=url))
    path = ArtifactoryPath(url, auth=(user, token))
    os.rename(artifact.file, file)
    try:
        path.deploy_file(file)
    except:
        os.rename(file, artifact.file)
        raise
    os.rename(file, artifact.file)
예제 #3
0
    def test_listdir(self):
        a = self.cls()

        # Directory
        path = ArtifactoryPath(
            "http://artifactory.local/artifactory/libs-release-local")

        constructed_url = (
            "http://artifactory.local/artifactory/api/storage/libs-release-local"
        )
        responses.add(
            responses.GET,
            constructed_url,
            status=200,
            json=self.dir_stat,
        )
        children = a.listdir(path)

        self.assertEqual(children, [".index", "com"])

        # Regular File
        path = ArtifactoryPath(
            "http://artifactory.local/artifactory/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz"
        )
        constructed_url = (
            "http://artifactory.local/artifactory/api/storage/libs-release-local"
        )
        responses.add(
            responses.GET,
            constructed_url,
            status=200,
            json=self.file_stat,
        )

        self.assertRaises(OSError, a.listdir, path)
예제 #4
0
def upload():
    path = ArtifactoryPath(
        "http://artifactory.calormen.net:8040/artifactory/generic-local/test",
        auth=('admin', 'f22-demo'))

    path.deploy_file("test.txt")

    path = ArtifactoryPath(
        "http://artifactory.calormen.net:8040/artifactory/generic-local/test/test_results",
        auth=('admin', 'f22-demo'))

    path.deploy_file("test-reports/results.xml")
    path.deploy_file("test-reports/results_download.xml")
    def get_latest_image(self,url):

        auth = str(
            base64.b64encode(
                bytes('%s:%s' % (cicd_user,cicd_pw ), 'utf-8')
            ),
            'ascii'
        ).strip()
        headers = {'Authorization': 'Basic ' + auth}

        ''' FIND THE LATEST FILE NAME'''
        print(url)
        req = urllib.request.Request(url, headers=headers)
        response = urllib.request.urlopen(req)
        html = response.read()
        soup = BeautifulSoup(html, features="html.parser")
        last_link = soup.find_all('a', href=True)[-1]
        latest_file=last_link['href']

        filepath = local_dir
        os.chdir(filepath)
        #file_url = url + latest_file

        ''' Download the binary file from Jfrog'''
        path = ArtifactoryPath(url,auth=(cicd_user, cicd_pw))
        path.touch()
        for file in path:
            print('File =', file)

        path = ArtifactoryPath(file, auth=(cicd_user, cicd_pw))
        print("file to be downloaded :" ,latest_file)
        print("File Path:",file)
        with path.open() as des:
            with open(latest_file, "wb") as out:
                out.write(des.read())
        des.close()
        print("Extract the tar.gz file and upgrade the AP ")
        housing_tgz = tarfile.open(latest_file)
        housing_tgz.extractall()
        housing_tgz.close()
        return "pass"
        print("Extract the tar file, and copying the file to  Linksys AP directory")
        #with open("/Users/syamadevi/Desktop/syama/ea8300/ap_sysupgrade_output.log", "a") as output:
         #   subprocess.call("scp /Users/syamadevi/Desktop/syama/ea8300/openwrt-ipq40xx-generic-linksys_ea8300-squashfs-sysupgrade.bin [email protected]:/tmp/openwrt-ipq40xx-generic-linksys_ea8300-squashfs-sysupgrade.bin",shell=True, stdout=output,
          #                 stderr=output)

        print('SSH to Linksys and upgrade the file')

        '''
예제 #6
0
    def __CreateArtifactoryPath(self, path=None):
        uri = self.__uri
        if not (path is None):
            uri += '/' + path

        if self.__apiKey is None:
            self.__RequestCredentials()
            path = ArtifactoryPath(uri,
                                   auth=(self.__username, self.__password),
                                   verify=self.__verifyCertificate)
        else:
            path = ArtifactoryPath(uri,
                                   apikey=self.__apiKey,
                                   verify=self.__verifyCertificate)
        return path
예제 #7
0
    def setUp(self):
        self.arti = ArtifactoryPath("http://b.com/artifactory")
        self.users_request_url = f"{self.arti.drive}/api/security/users"
        self.users = [
            {
                "name": "user_1",
                "uri": "http://b.com/artifactory/api/security/users/user_1",
                "realm": "internal",
            },
            {
                "name": "user_2",
                "uri": "http://b.com/artifactory/api/security/users/user_2",
                "realm": "internal",
            },
        ]
        self.user_1 = {"name": "user_1", "email": "*****@*****.**"}
        self.user_2 = {"name": "user_2", "email": "*****@*****.**"}

        self.groups_request_url = f"{self.arti.drive}/api/security/groups"
        self.groups = [
            {
                "name": "group_1",
                "uri": "http://b.com/artifactory/api/security/groups/group_1",
            },
            {
                "name": "group_2",
                "uri": "http://b.com/artifactory/api/security/groups/group_2",
            },
        ]
        self.group_1 = {
            "name": "group_1",
            "realm": "internal",
        }
        self.group_2 = {
            "name": "group_2",
            "realm": "internal",
        }

        self.projects_request_url = (
            f"{self.arti.drive.rstrip('/artifactory')}/access/api/v1/projects")
        self.projects = [
            {
                "project_key": "project_key_1",
                "description": "description_1",
            },
            {
                "project_key": "project_key_2",
                "description": "description_2",
            },
        ]
        self.project_1 = {
            "project_key": "project_key_1",
            "description": "description_1",
            "admin_privileges": {},
        }
        self.project_2 = {
            "project_key": "project_key_2",
            "description": "description_2",
            "admin_privileges": {},
        }
예제 #8
0
def artifactory_aql(artifactory_url, username, password, kerberos,
                    aql_query_dict, verify):
    """
    Send AQL to Artifactory and get list of Artifacts
    :param artifactory_url:
    :param username:
    :param password:
    :param kerberos: Boolean if kerberos authentication should be used
    :param verify: Boolean if SSL certificate should be checked
    :param aql_query_dict:
    :param max_depth_print:
    :param human_readable:
    :param all:
    :return:
    """
    if kerberos:
        auth = HTTPKerberosAuth(mutual_authentication=DISABLED,
                                sanitize_mutual_error_response=False)
    else:
        if not password:
            raise ValueError(
                "argument 'password' needs to be set for basic authentication")
        auth = (username, password)
    aql = ArtifactoryPath(artifactory_url, auth=auth, verify=verify)

    logging.debug("AQL query: items.find({})".format(aql_query_dict))
    artifacts = aql.aql('items.find', aql_query_dict)
    logging.debug('Artifacts count: {}'.format(len(artifacts)))
    artifacts_size = sum([x['size'] for x in artifacts])
    logging.debug('Summary size: {}'.format(size(artifacts_size)))

    return artifacts
예제 #9
0
def update_updi_devices():
    """
    Update all UPDI devices
    """
    packs = []
    PACKS_OF_INTEREST = [
        'Microchip.ATmega_DFP', 'Microchip.ATtiny_DFP', 'Microchip.AVR-'
    ]
    # Search entire path
    from artifactory import ArtifactoryPath
    path = ArtifactoryPath("{}/{}/{}".format(MICROCHIP_ARTIFACTORY_SERVER_URL,
                                             REPO, ORG))
    for a in path:
        name = str(a)
        packname = name.split('/')[-1]
        # Filter:
        if name.endswith("_DFP") and not name.endswith(
                "ENG_DFP") and packname.startswith(tuple(PACKS_OF_INTEREST)):
            packs.append(packname)

    for pack in packs:
        print("Fetching latest pack for: {}".format(pack))
        temp_dir = tempfile.TemporaryDirectory()
        pack_info = fetch_latest_device_atpack(pack, temp_dir.name)
        devices = os.listdir(temp_dir.name.replace("\\", "/") + "/atdf")
        print("Parsing {} devices".format(len(devices)))
        for device in devices:
            add_new_updi_device(
                temp_dir.name.replace("\\", "/") + "/atdf/" + device,
                pack_info)
        print("Success.")
예제 #10
0
    def get_raw_manifest_list(self):
        """Return the docker manifest list in json format
        Raises:
            ManifestListNotFound: [description]
        Returns:
            DICT: manifest.list.json content
        """

        listpath = '/'.join([
            self.artifactory_base,
            self._get_artifactory_repo(
            ),  # We have to massage the repo for artifactory
            self.image.get_image_name(),
            self.image.get_tag(),
            "list.manifest.json"
        ])
        list_path = ArtifactoryPath(listpath,
                                    auth=(self.artifactory_user,
                                          self.artifactory_key))

        try:
            f = list_path.open()
        except FileNotFoundError as e:
            raise ManifestListNotFound(e)
        except RuntimeError as e:
            raise ManifestListNotFound(e)
        return json.loads(f.read().decode('utf-8'))
예제 #11
0
파일: api.py 프로젝트: audeering/audfactory
def path(url: str, ) -> ArtifactoryPath:
    r"""Authenticate at Artifactory and get path object.

    You can set your username and API key in the console:

    .. code-block:: bash

        $ export ARTIFACTORY_USERNAME=...
        $ export ARTIFACTORY_API_KEY=...

    If they are not specified,
    they are read from :file:`~/.artifactory_python.cfg`
    by matching ``url`` against the available entries
    to pick the matching Artifactory server.

    Args:
        url: URL to path on Artifactory

    Returns:
        Artifactory path object similar to pathlib.Path

    Example:
        >>> artifactory_path = path(
        ...     'https://audeering.jfrog.io/artifactory/data-public/emodb/'
        ... )
        >>> for content in artifactory_path:
        ...     print(os.path.basename(str(content)))
        ...
        db
        media
        meta

    """
    username, apikey = authentification(url)
    return ArtifactoryPath(url, auth=(username, apikey))
예제 #12
0
 def connect(self):
     
     self.client = ArtifactoryPath(
         os.path.join(self.address, 'artifactory', self.repo),
         auth=(self.compass.user, self.compass.pswd),
         verify=self.compass.check_certificate
     )
예제 #13
0
    def load(self, file_path: Pathlike) -> Estimator:
        """
        Loads a pickled estimator from given filepath and returns the estimator

        Parameters
        ----------
        file_path: Pathlike
            Path to load the estimator relative to ArtifactoryStorage

        Example
        -------
        We can load a saved pickled estimator from disk directly from Artifactory:

            storage = ArtifactoryStorage('http://artifactory.com', 'path/to/repo')
            my_estimator = storage.load('estimatorfile')

        We now have a trained estimator loaded.

        Returns
        -------
        Object
            estimator unpickled object
        """
        if ArtifactoryPath(file_path).is_file():
            artifactory_path = file_path
        else:
            artifactory_path = self.artifactory_path / file_path

        with artifactory_path.open() as f:
            by = BytesIO()
            by.write(f.read())
            by.seek(0)
            return joblib.load(by)
예제 #14
0
    def test_unlink_raises_on_404(self):
        """
        Test that folder/file unlink raises exception if we checked that file
        exsists and we still get 404. This is a result of permission issue
        """
        path = ArtifactoryPath(
            "http://artifactory.local/artifactory/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz"
        )
        constructed_url = (
            "http://artifactory.local/artifactory"
            "/api/storage"
            "/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz")
        responses.add(
            responses.GET,
            constructed_url,
            status=200,
            json=self.file_stat,
        )

        responses.add(
            responses.DELETE,
            str(path),
            status=404,
        )

        with self.assertRaises(ArtifactoryException) as context:
            path.unlink()

        self.assertTrue(
            "insufficient Artifactory privileges" in str(context.exception))
예제 #15
0
파일: deploy.py 프로젝트: bgyori/pybel
def _deploy_helper(filename, module_name, get_module, get_today_fn, hash_check=True, auth=None):
    """Deploys a file to the Artifactory BEL namespace cache

    :param str filename: The physical path
    :param str module_name: The name of the module to deploy to
    :param tuple[str] auth: A pair of (str username, str password) to give to the auth keyword of the constructor of
                            :class:`artifactory.ArtifactoryPath`. Defaults to the result of :func:`get_arty_auth`.
    :return: The resource path, if it was deployed successfully, else none.
    :rtype: Optional[str]
    """
    path = ArtifactoryPath(
        get_module(module_name),
        auth=get_arty_auth() if auth is None else auth
    )
    path.mkdir(exist_ok=True)

    if hash_check:
        deployed_semantic_hashes = {
            get_bel_resource_hash(subpath.as_posix())
            for subpath in path
        }

        semantic_hash = get_bel_resource_hash(filename)

        if semantic_hash in deployed_semantic_hashes:
            return  # Don't deploy if it's already uploaded

    target = path / get_today_fn(module_name)
    target.deploy_file(filename)

    log.info('deployed %s', module_name)

    return target.as_posix()
예제 #16
0
    def test_unlink(self):
        """
        Test that folder/file unlink works
        """
        path = ArtifactoryPath(
            "http://artifactory.local/artifactory/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz"
        )
        constructed_url = (
            "http://artifactory.local/artifactory"
            "/api/storage"
            "/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz")
        responses.add(
            responses.GET,
            constructed_url,
            status=200,
            json=self.file_stat,
        )

        responses.add(
            responses.DELETE,
            str(path),
            status=200,
        )

        path.unlink()
예제 #17
0
    def test_stat_no_sha256(self):
        """
        Test file stats. No sha256 checksum is available.
        Check that stat() works on instance itself
        :return:
        """

        # Regular File
        path = ArtifactoryPath(
            "http://artifactory.local/artifactory/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz"
        )
        constructed_url = (
            "http://artifactory.local/artifactory"
            "/api/storage"
            "/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz")
        file_stat = {
            "repo": "ext-release-local",
            "path": "/org/company/tool/1.0/tool-1.0.tar.gz",
            "created": "2014-02-24T21:20:59.999+04:00",
            "createdBy": "someuser",
            "lastModified": "2014-02-24T21:20:36.000+04:00",
            "modifiedBy": "anotheruser",
            "lastUpdated": "2014-02-24T21:20:36.000+04:00",
            "downloadUri":
            "http://artifactory.local/artifactory/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz",
            "mimeType": "application/octet-stream",
            "size": "26776462",
            "checksums": {
                "sha1": "fc6c9e8ba6eaca4fa97868ac900570282133c095",
                "md5": "2af7d54a09e9c36d704cb3a2de28aff3",
            },
            "originalChecksums": {
                "sha1": "fc6c9e8ba6eaca4fa97868ac900570282133c095",
                "md5": "2af7d54a09e9c36d704cb3a2de28aff3",
            },
            "uri": constructed_url,
        }

        responses.add(
            responses.GET,
            constructed_url,
            status=200,
            json=file_stat,
        )

        stats = path.stat()
        self.assertEqual(
            stats.ctime,
            dateutil.parser.parse("2014-02-24T21:20:59.999+04:00"))
        self.assertEqual(
            stats.mtime,
            dateutil.parser.parse("2014-02-24T21:20:36.000+04:00"))
        self.assertEqual(stats.created_by, "someuser")
        self.assertEqual(stats.modified_by, "anotheruser")
        self.assertEqual(stats.mime_type, "application/octet-stream")
        self.assertEqual(stats.size, 26776462)
        self.assertEqual(stats.sha1,
                         "fc6c9e8ba6eaca4fa97868ac900570282133c095")
        self.assertEqual(stats.sha256, None)
예제 #18
0
def get_latest_version_age(artifact_url):
    path = ArtifactoryPath(artifact_url, apikey=api_key)
    properties = ArtifactoryPath.stat(path)
    modification_date = properties.mtime
    # removing timezone
    modification_date = modification_date.replace(tzinfo=None)
    aritfact_age = today - modification_date
    return aritfact_age.days
예제 #19
0
파일: arty.py 프로젝트: bgyori/pybel
def _get_path_helper(module_name, getter):
    """Helps get the Artifactory path for a certain module

    :param str module_name: The name of the module
    :param types.FunctionType getter: The function that gets the modules from the Artifactory repository
    :rtype: artifactory.ArtifactoryPath
    """
    return ArtifactoryPath(getter(module_name))
예제 #20
0
def download():
    path = ArtifactoryPath(
        "http://artifactory.calormen.net:8040/artifactory/generic-local/test/numbers.txt",
        auth=('admin', 'f22-demo'))

    with path.open() as fd:
        with open("old.txt", "wb") as out:
            out.write(fd.read())
예제 #21
0
def generate_url(artifactory_url: str,
                 repo: str,
                 apikey=None,
                 auth=None) -> "ArtifactoryPath":
    if not artifactory_url.endswith("artifactory"):
        artifactory_url = f"{artifactory_url}/artifactory"

    return ArtifactoryPath(f"{artifactory_url}/{repo}",
                           apikey=apikey,
                           auth=auth)
예제 #22
0
    def search_packages_dohq(self, url, dictionary={}, **kwargs):
        """
		This method parses the remote artifactory repository structure
			and chooses those files that are having the extension .py or .whl.
			Uses dohq-artifactory API.
		:param url (str): remote artifactory repository address
		:param dictionary (dict): dict where will be passed the result
		:**kwargs may contain 'login', 'password'
		:return dict formatted like {'module link', 'module name'}
		"""
        if 'login' in kwargs and 'password' in kwargs:
            path = ArtifactoryPath(url,
                                   auth=(kwargs['login'], kwargs['password']))
        else:
            path = ArtifactoryPath(url, auth=(self.login, self.password))

        for p in path.glob('**/*.*'):
            link = str(p)
            if link.endswith('.py') or link.endswith('.whl'):
                dictionary.update({link: link.split('/')[-1]})

        return dictionary
예제 #23
0
def delete_old_artifacts(list_of_artifacts, chart_name):
    """Extract list of artifacts for this chart and only keep the last NUMBER_OF_CHARTS versions uploaded"""
    artifacts = list(filter(lambda x: chart_name == x[0], list_of_artifacts))
    if len(artifacts) < NUMBER_OF_CHARTS:
        return
    artifacts.sort(key=lambda date: time.strptime(date[1], '%d-%b-%Y %H:%M'))
    for artifact in artifacts[:-(NUMBER_OF_CHARTS + 1)]:
        artifact_path = ArtifactoryPath(
            full_artifactory_url + artifact[2],
            auth=credentials,
        )
        if artifact_path.exists():
            print("Deleting artifact " + artifact[2])
            artifact_path.unlink()
예제 #24
0
    def _get_raw_image_digest(self):
        manifestpath = '/'.join([
                        self.artifactory_base,
                        self._get_artifactory_repo(), # We have to massage the repo for artifactory
                        self.image.get_image_name(),
                        self.image.get_tag(),
                        "manifest.json"
                    ])
        manifest_path = ArtifactoryPath(manifestpath, auth=(self.artifactory_user, self.artifactory_key))

        try:
            return ArtifactoryPath.stat(manifest_path).sha256
        except FileNotFoundError as e:
            raise ManifestNotFound(e)
예제 #25
0
def getArtifact(type, version, artifact, dest):
    target = os.path.join(dest, '%s-%s.jar' % (artifact, version))
    if not os.path.exists(target):
        print('Downloading artifact %s-%s.jar' % (artifact, version))

        baseurl = '%s/%s/%s' % (ARTIFACTORY_URL, type.repositoryId,
                                type.groupId.replace('.', '/'))
        url = '%s/%s/%s/%s-%s.jar' % (baseurl, artifact, version, artifact,
                                      version)
        path = ArtifactoryPath(
            url,
            auth=(os.environ.get('CORDA_ARTIFACTORY_USERNAME'),
                  os.environ.get('CORDA_ARTIFACTORY_PASSWORD')))
        with path.open() as fd:
            with open(target, 'wb') as out:
                out.write(fd.read())
    return target
예제 #26
0
 def _create_archive_obj(self):
     """
     Create archive object for tests.
     During archive creation we call stats() to check if it is_dir(), thus, mock response
     :return:
     """
     ArtifactoryPath = self.cls
     folder = ArtifactoryPath("http://b/artifactory/reponame/folder")
     constructed_url = "http://b/artifactory/api/storage/reponame/folder"
     responses.add(
         responses.GET,
         constructed_url,
         status=200,
         json=self.dir_stat,
     )
     archive_obj = folder.archive(check_sum=True)
     return archive_obj
예제 #27
0
def integration_artifactory_path_repo(artifactory):
    """
    Create repo if not exist and remove all files from this repo
    :param artifactory:
    :return:
    """
    # Create repo if not exist
    name = 'integration-artifactory-path-repo'
    repo_ = artifactory.find_repository_local(name)
    if repo_ is None:
        repo_ = RepositoryLocal(artifactory=artifactory, name=name)
        repo_.create()

    # Remove all file from repo
    repo_path = ArtifactoryPath(str(artifactory) + "/" + name, auth=artifactory.auth)
    for path_ in repo_path.glob('*'):
        path_.unlink()
    yield repo_
예제 #28
0
    def _collect_docker_size(self, new_result):
        docker_repos = list(set(x['repo'] for x in new_result))

        if docker_repos:
            aql = ArtifactoryPath(self.artifactory_server,
                                  session=self.artifactory_session)
            args = [
                'items.find', {
                    "$or": [{
                        "repo": repo
                    } for repo in docker_repos]
                }
            ]
            artifacts_list = aql.aql(*args)

            for artifact in new_result:
                artifact['size'] = sum([
                    docker_layer['size'] for docker_layer in artifacts_list
                    if docker_layer['path'] == '{}/{}'.format(
                        artifact['path'], artifact['name'])
                ])
예제 #29
0
 def _mock_properties_response():
     """
     Function to mock responses on HTTP requests
     :return: ArtifactoryPath instance object
     """
     # Regular File
     path = ArtifactoryPath(
         "http://artifactory.local/artifactory/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz"
     )
     constructed_url = (
         "http://artifactory.local/artifactory"
         "/api/storage/"
         "ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz")
     reference_props = {
         "test": ["test_property"],
         "removethis": ["removethis_property"],
         "time": ["2018-01-16 12:17:44.135143"],
     }
     responses.add(
         responses.GET,
         constructed_url,
         status=204,
         json={
             "properties": reference_props,
             "uri": constructed_url,
         },
     )
     responses.add(
         responses.DELETE,
         constructed_url,
         status=204,
         body="",
     )
     responses.add(
         responses.PUT,
         constructed_url,
         status=204,
         body="",
     )
     return path
예제 #30
0
    def test_unlink_raises_not_found(self):
        """
        Test that folder/file unlink raises OSError if file does not exist
        """
        path = ArtifactoryPath(
            "http://artifactory.local/artifactory/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz"
        )
        constructed_url = (
            "http://artifactory.local/artifactory"
            "/api/storage"
            "/ext-release-local/org/company/tool/1.0/tool-1.0.tar.gz")
        responses.add(
            responses.GET,
            constructed_url,
            status=404,
            body="Unable to find item",
        )
        with self.assertRaises(OSError) as context:
            path.unlink()

        self.assertTrue(
            "No such file or directory" in context.exception.strerror)