Beispiel #1
0
class XMPTestCase(unittest.TestCase):
	def setUp(self):
		self.EXPECTED_NS_UIDS = fixtures.JPG_PHOTO_NS_UIDS

		self.xmp_file = XMPFile(fixtures.sandboxedData(fixtures.JPG_PHOTO))
		self.xmp_file.__enter__()
		self.example_xmp = self.xmp_file.metadata

	def tearDown(self):
		self.xmp_file.__exit__(None, None, None)
Beispiel #2
0
	def test_setattr_existing(self):
		jpg_filepath = fixtures.sandboxedData(fixtures.JPG_PHOTO)

		with XMPFile(jpg_filepath, rw=True) as xmp_file:
			xmp_file.metadata[libxmp.consts.XMP_NS_EXIF].FNumber = "314/141"
			self.assertEqual(xmp_file.metadata[libxmp.consts.XMP_NS_EXIF].FNumber.value, "314/141")

		with XMPFile(jpg_filepath) as xmp_file:
			# libxmp doesn't allow persistence of standard namespaces such as EXIF
			self.assertEqual(xmp_file.metadata[libxmp.consts.XMP_NS_EXIF].FNumber.value, "32/10")
Beispiel #3
0
	def test_setattr_inexistent(self):
		sandboxed_photo = fixtures.sandboxedData(fixtures.JPG_PHOTO)
		with XMPFile(sandboxed_photo, rw=True) as xmp_file:
			xmp_file.metadata[TEST_NS].inexistent_element = 12
			self.assertIsInstance(xmp_file.metadata[TEST_NS].inexistent_element,
			                      XMPValue)
			self.assertEqual(xmp_file.metadata[TEST_NS].inexistent_element.value, "12")
		# Close and write file then reopen it
		with XMPFile(sandboxed_photo) as xmp_file:
			self.assertIsInstance(xmp_file.metadata[TEST_NS].inexistent_element,
			                      XMPValue)
			self.assertEqual(xmp_file.metadata[TEST_NS].inexistent_element.value, "12")
Beispiel #4
0
    def __init__(self, folder_path, mode="r"):
        """
		Open a QiDataSet.

		:param folder_path: path to the data set to open
		:type folder_path: str
		:param mode: opening mode, "r" for reading, "w" for writing
		:type mode: str

		.. warnings::

			The folder must exist and cannot be created if it does not.

		.. note::

			"r" mode means the folder is expected to contain a file named
			"metadata.xmp" of a certain form (an empty file is not enough).
			Otherwise, opening will fail.
			In "w" mode, any folder can be opened. If no "metadata.xmp" file is
			present, one will be created. If there is one, it WILL NOT BE TRUNCATED
			(unlike the regular "w" mode of file opening).
		"""
        if not os.path.isdir(folder_path):
            raise IOError("%s is not a valid folder" % folder_path)

        self._folder_path = folder_path
        metadata_path = os.path.join(folder_path, METADATA_FILENAME)
        if not isDataset(folder_path):
            if mode == "r":
                # This is not an existing qidata dataset and we are not allowed
                # to create it
                raise IOError(
                    "Given path is not a QiData dataset: %s does not exist" %
                    (METADATA_FILENAME))
            elif mode == "w":
                # Folder is not a data set but we can turn it into one

                # We need XMP to create an empty metadata.xmp
                # Open it with xmp so that metadata.xmp is created
                with XMPFile(metadata_path, rw=True):
                    pass

        self._annotation_content = dict()
        self._files_type = dict()
        self._xmp_file = XMPFile(metadata_path, rw=(mode == "w"))
        self._is_closed = True
        self._streams = dict()
        self._frames = list()
        self._open()
Beispiel #5
0
    def __init__(self, file_path, mode="r"):
        """
		Create and open a QiDataFile.
		QiDataFile wraps the xmp library specifically to store QiDataObjects under the
		QiData namespace.

		:param file_path: path to the file to open
		:type file_path: str
		:param mode: opening mode, "r" for reading, "w" for writing
		:param mode: str

		.. warnings::
			The mode behavior is different from the regular Python file mode.
			The file is NEVER created if it does not exist. Besides, opening
			an existing file in "w" mode does not overwrite it.
		"""
        if os.path.splitext(file_path)[1] == ".xmp":
            # If file is a .xmp, just read it normally
            # If the filename without xmp extension is an existing
            # file, then mark it as the real file opened
            xmp_path = file_path
            if os.path.exists(os.path.splitext(file_path)[0]):
                file_path = os.path.splitext(file_path)[0]

        elif os.path.exists(file_path + ".xmp"):
            # If there is an external annotation file, use it
            xmp_path = file_path + ".xmp"

        elif mode == "w":
            # If there is no external annotation file but we are in "w" mode
            # Copy the internal annotations in an external annotation file
            xmp_path = file_path + ".xmp"
            with XMPFile(file_path, rw=False) as _internal:
                with XMPFile(xmp_path, rw=True) as _external:
                    _external.libxmp_metadata = _internal.libxmp_metadata
        else:
            # Open the internal annotations
            xmp_path = file_path

        # Store the file path
        self._file_path = file_path

        # And prepare the xmp file
        self._xmp_file = XMPFile(xmp_path, rw=(mode == "w"))
        self._is_closed = True
        self._open()
Beispiel #6
0
	def test_modify_readwrite(self):
		original_sha1 = sha1(self.jpg_path)
		with XMPFile(self.jpg_path, rw=True) as f:
			tst_prefix = f.libxmp_metadata.get_prefix_for_namespace(TEST_NS)
			f.libxmp_metadata.set_property(schema_ns=TEST_NS,
			                               prop_name=tst_prefix+"Property",
			                               prop_value="Value")
		modified_sha1 = sha1(self.jpg_path)
		self.assertNotEqual(original_sha1, modified_sha1)
Beispiel #7
0
	def test_xmp_file_open(self):
		with XMPFile(fixtures.sandboxedData("foo.xmp")) as file:
			namespaces = file.metadata.namespaces
			self.assertIsInstance(namespaces, collections.Sequence)
			self.assertEqual(len(namespaces), 1)

			namespace = namespaces[0]
			self.assertIsInstance(namespace, XMPNamespace)
			self.assertEqual(namespace.uid, "http://test.com/xmp/test/1")
			self.assertIsInstance(namespace.structure, XMPValue)
			self.assertEqual(namespace.structure.value, "value")
Beispiel #8
0
	def test_modify_readonly(self):
		import warnings
		with warnings.catch_warnings(record=True) as w:
			warnings.simplefilter("always")
			with XMPFile(self.jpg_path) as f:
				tst_prefix = f.libxmp_metadata.get_prefix_for_namespace(TEST_NS)
				f.libxmp_metadata.set_property(schema_ns=TEST_NS,
				                               prop_name=tst_prefix+"Property",
				                               prop_value="Value")
			self.assertEqual(len(w), 1)
			self.assertEqual(w[-1].category, RuntimeWarning)
Beispiel #9
0
class Metadata(unittest.TestCase):
	def setUp(self):
		self.xmp_file = XMPFile(fixtures.sandboxedData(fixtures.JPG_PHOTO))
		self.xmp_file.open()
		self.xmp_metadata = self.xmp_file.metadata[TEST_NS]

	def tearDown(self):
		self.xmp_file.close()

	def test_virtual_element(self):
		self.xmp_metadata.inexistent_attribute

	def test_nested_virtual_element(self):
		self.xmp_metadata.inexistent_attribute.nested_inexistent_attribute

	def test_virtual_element_descriptor_get(self):
		self.assertIsInstance(self.xmp_metadata.inexistent_attribute, XMPVirtualElement)
		self.assertIsInstance(self.xmp_metadata.__dict__, dict)

	def test_virtual_element_descriptor_set_readonly(self):
		self.xmp_metadata.inexistent_attribute = 12
		import warnings
		with warnings.catch_warnings(record=True) as w:
			warnings.simplefilter("always")
			self.xmp_file.close()
			self.assertEqual(len(w), 1)
			self.assertEqual(w[-1].category, RuntimeWarning)

		self.xmp_file.open()

	def test_virtual_element_descriptor_set(self):
		with XMPFile(fixtures.sandboxedData(fixtures.JPG_PHOTO), rw = True) as xmpitem:
			xmpitem.metadata[TEST_NS].inexistent_attribute = 12
			self.assertIsInstance(xmpitem.metadata[TEST_NS].inexistent_attribute, XMPValue)
			self.assertEqual(xmpitem.metadata[TEST_NS].inexistent_attribute.value, "12")

	def test_virtual_element_descriptor_delete(self):
		with self.assertRaises(TypeError):
			del self.xmp_metadata.inexistent_attribute
Beispiel #10
0
class QiDataSet(object):
    class AnnotationStatus(_BaseEnum):
        """
		AnnotationStatus represents the completeness of an annotation.
		For instance, if the "Face" metadata of "jdoe" is TOTAL, it means that
		all files have been annotated. This is a very valuable information.

		:Example:
			Imagine a file in the dataset has no "Face" annotation. Does it
			mean that there is no face visible in the file, or that the
			annotator forgot to annotate that specific file ? When the
			annotation is registered as TOTAL, it means that every file
			without a "Face" annotation has actually no face in it.
		"""
        PARTIAL = 0
        TOTAL = 1

    # ───────────
    # Constructor

    def __init__(self, folder_path, mode="r"):
        """
		Open a QiDataSet.

		:param folder_path: path to the data set to open
		:type folder_path: str
		:param mode: opening mode, "r" for reading, "w" for writing
		:type mode: str

		.. warnings::

			The folder must exist and cannot be created if it does not.

		.. note::

			"r" mode means the folder is expected to contain a file named
			"metadata.xmp" of a certain form (an empty file is not enough).
			Otherwise, opening will fail.
			In "w" mode, any folder can be opened. If no "metadata.xmp" file is
			present, one will be created. If there is one, it WILL NOT BE TRUNCATED
			(unlike the regular "w" mode of file opening).
		"""
        if not os.path.isdir(folder_path):
            raise IOError("%s is not a valid folder" % folder_path)

        self._folder_path = folder_path
        metadata_path = os.path.join(folder_path, METADATA_FILENAME)
        if not isDataset(folder_path):
            if mode == "r":
                # This is not an existing qidata dataset and we are not allowed
                # to create it
                raise IOError(
                    "Given path is not a QiData dataset: %s does not exist" %
                    (METADATA_FILENAME))
            elif mode == "w":
                # Folder is not a data set but we can turn it into one

                # We need XMP to create an empty metadata.xmp
                # Open it with xmp so that metadata.xmp is created
                with XMPFile(metadata_path, rw=True):
                    pass

        self._annotation_content = dict()
        self._files_type = dict()
        self._xmp_file = XMPFile(metadata_path, rw=(mode == "w"))
        self._is_closed = True
        self._streams = dict()
        self._frames = list()
        self._open()

    # ──────────
    # Properties

    @property
    def annotations_available(self):
        """
		Returns a list of all annotations references that are present at least
		once in the dataset, with their status

		:Example:
			>>> with QiDataSet("dummy/dataset", "r") as d:
			>>>     d.annotations_available
			>>> {("jdoe", "Property"): QiDataSet.AnnotationStatus.PARTIAL)}
		"""
        return copy.deepcopy(self._annotation_content)

    @property
    def annotators(self):
        """
		Returns the list of people who annotated at least one file of the
		dataset
		"""
        return set([i[0] for i in self._annotation_content])

    @property
    def children(self):
        """
		Return the list of supported files contained by the data set.
		"""
        ret = [
            fn for fn in os.listdir(self.name)
            if (qidata.isSupportedDataFile(fn))
        ]
        ret.sort()
        return ret

    @property
    def context(self):
        """
		Describes the context around the data sets.

		:rtype: qidata.metadata_objects.context.Context
		"""
        return self._context

    @context.setter
    @throwIfReadOnly
    def context(self, new_context):
        if isinstance(new_context, Context):
            self._context = new_context
        else:
            raise TypeError("Wrong type given to update context property")

    @property
    def datatypes_available(self):
        """
		Returns a list of all data types present in the dataset
		"""
        return set([DataType[i] for i in self._files_type.keys()])

    @property
    def mode(self):
        """
		Specify the file mode

		"r" => read-only mode
		"w" => read/write mode
		"""
        return "w" if self._xmp_file.rw else "r"

    @property
    def read_only(self):
        return ("r" == self.mode)

    @property
    def name(self):
        """
		Give the folder path
		"""
        return self._folder_path

    # ──────────
    # Public API

    def createNewStream(self, name, timestamp_file_pairs):
        """
		Creates a new set of files which should be considered as part
		of the same data stream

		:param name: Name given to the stream
		:type name: str
		:param timestamp_file_pairs: List of pairs of filename and timestamp
		:type timestamp_file_pairs: list
		:raises: AttributeError if an empty list is given
		:raises: TypeError if the given files have different types
		"""
        if len(timestamp_file_pairs) == 0:
            raise AttributeError(
                "At least one file is needed to create a stream")
        with self.openChild(timestamp_file_pairs[0][1]) as f:
            data_type = f.type
        for i in range(1, len(timestamp_file_pairs)):
            if timestamp_file_pairs[i][1] in self._files_type[str(data_type)]:
                continue
            with self.openChild(timestamp_file_pairs[i][1]) as f:
                if data_type == f.type:
                    continue
            raise TypeError("Given files are not all of the same type")
        self._streams[name] = (data_type, dict(timestamp_file_pairs))

    def close(self):
        """
		Closes the dataset after writing the metadata
		"""
        if self.mode != "r":
            # Erase current dataset content's metadata
            _raw_metadata = self._xmp_file.metadata[QIDATA_CONTENT_NS]
            for key in _raw_metadata.attributes():
                del _raw_metadata[key]

            # Save new dataset content's metadata
            for (key, value) in self._annotation_content.iteritems():
                setattr(_raw_metadata.annotation_content, key[0],
                        {key[1]: value})

            setattr(_raw_metadata, "files_type", self._files_type)

            setattr(_raw_metadata, "context", self.context)

            # Save data streams (they need to be a little be reworked to fit
            # XMP base rules, namely numbers cannot be keys so we add the
            # letter "t" in front of the timestamps)
            tmp_streams = dict()
            for stream_name, stream in self._streams.iteritems():
                tmp_streams[stream_name] = (stream[0], dict())
                for (timestamp, filename) in stream[1].iteritems():
                    tmp_streams[stream_name][1]["t%d.%09d" %
                                                timestamp] = filename

            setattr(_raw_metadata, "streams", tmp_streams)

        self._xmp_file.close()
        for f in self._frames:
            f.close()
        self._is_closed = True

    def examineContent(self):
        """
		Examine all dataset's files to infer content information.

		For every supported file contained in the dataset, this function will:
		 - check if a DataType is already defined and infer one from the file
		   extension if not.
		 - open the file and look for present annotations

		Once all files have been studied, remaining annotations will be updated
		with any known status that might have been present before this function
		was called.
		"""
        _annotation_content = dict()
        self._files_type = dict()
        for name in self.children:
            path = os.path.join(self._folder_path, name)
            with qidata.open(path, "r") as _f:
                for annotator, annotations in _f.annotations.iteritems():
                    for annotation_type in annotations.keys():
                        _annotation_content[(
                            annotator, annotation_type
                        )] = QiDataSet.AnnotationStatus.PARTIAL
                if not self._files_type.has_key(str(_f.type)):
                    self._files_type[str(_f.type)] = []
                self._files_type[str(_f.type)].append(name)

        for _f in self.getAllFrames():
            for annotator, annotations in _f.annotations.iteritems():
                for annotation_type in annotations.keys():
                    _annotation_content[(
                        annotator,
                        annotation_type)] = QiDataSet.AnnotationStatus.PARTIAL

        # For all discovered annotation, grab the previously known status
        # If an annotation had a status before but was not seen, it does
        # need to be kept only if it was declared as TOTAL
        for key in self._annotation_content:
            if QiDataSet.AnnotationStatus.TOTAL == self._annotation_content[
                    key]:
                _annotation_content[key] = QiDataSet.AnnotationStatus.TOTAL

        self._annotation_content = _annotation_content
        # files_info = dict()

        # # Keep track of the knowledge we have so far
        # if not hasattr(self, "_content"):
        # 	type_map = dict()
        # 	known_status = dict()
        # else:
        # 	type_map = self._content._type_content
        # 	known_status = self._content._data

        # supported_subpaths = self.children
        # for path in supported_subpaths:
        # 	if isDataset(path):
        # 		# Avoid it for the moment
        # 		# But later we will have to handle sub-datasets
        # 		continue

        # 	# Search if a specific type was set for this file
        # 	# If it does, then use it
        # 	# Otherwise, infer its type from the file extension
        # 	for data_type, file_list in type_map.iteritems():
        # 		if path in file_list:
        # 			file_type = data_type
        # 			break
        # 	else:
        # 		file_type = qidatafile.getFileDataType(path)

        # 	# And then add that file in the appropriate category
        # 	if not files_info.has_key(str(file_type)):
        # 		files_info[str(file_type)] = []
        # 	files_info[str(file_type)].append(path)

        # # Finally, open the file to look for annotations
        # with self.openChild(path, "r") as _child:
        # 	for child_annotator in _child.annotations.keys():
        # 			if not annotations_info.has_key(child_annotator):
        # 				annotations_info[child_annotator] = dict()
        # 			for metadata_type in _child.metadata[child_annotator]:
        # 				annotations_info[child_annotator][metadata_type] = False

        # # Create new content
        # self._content = QiDataSetContent(files_info, annotations_info)

        # # For each TOTAL status, update the new content
        # # Indeed, a TOTAL status is an input from a human and therefore must
        # # not be erased by the program.
        # # Any PARTIAL status present before and not in the new
        # # content would mean that NO file was found with this
        # # annotation from this person => annotations were probably
        # # removed => no need to re-add the information.
        # for key, value in known_status.iteritems():
        # 	if value == True:
        # 		self._content._data[key] = True

    @staticmethod
    def filter(dataset_list,
               only_annotated_by=None,
               only_with_annotations=None,
               only_total_annotations=False):
        """
		Filters out dataset not fitting the given criteria.

		:param dataset_list: List of folders to filter
		:type dataset_list: list
		:param only_annotated_by: List of requested annotators
		:type only_annotated_by: list
		:param only_with_annotations: List of requested annotation types
		:type only_with_annotations: list
		:param only_total_annotations: States if only total annotations should
		be considered
		:type only_total_annotations: bool

		:Example:
			The following command will only accept datasets containing total
			"Property" annotations made by "jdoe" or "jsmith"
			>>> QiDataSet.filter(
			...     dataset_lists,["jdoe", "jsmith"],["Property"], True)

			The following command will only accept datasets containing "Dummy"
			and "Property" annotations (total or partial) made by "jdoe"
			exclusively
			>>> QiDataSet.filter(
			...     dataset_lists,["jdoe"],["Property","Dummy"], False)
		"""
        filtered = []

        for dataset_path in dataset_list:
            with QiDataSet(dataset_path, "r") as ds:
                for a_ref, a_status in ds.annotations_available.iteritems():
                    if only_total_annotations\
                       and a_status==QiDataSet.AnnotationStatus.PARTIAL:
                        continue
                    if only_annotated_by is not None\
                       and not a_ref[0] in only_annotated_by:
                        continue
                    if only_with_annotations is not None\
                       and not a_ref[1] in only_with_annotations:
                        continue
                    filtered.append(dataset_path)
                    break
        return filtered

    def getAllFilesOfType(self, type_name):
        """
		Returns all the file names of a specific type

		:param type_name: Requested type
		:type type_name: ``qidata.DataType`` or str
		:return: List of filenames
		"""
        try:
            return copy.deepcopy(self._files_type[str(type_name)])
        except KeyError:
            # Check given type
            try:
                _ = DataType[str(type_name)]
            except KeyError:
                raise TypeError("%s is not a valid DataType" % type_name)
            # Type name is valid, but there is no file associated to it
            return []

    def getAllStreams(self):
        """
		Returns all declared streams

		:return: Every stream known by the data set
		:rtype: dict
		"""
        return copy.deepcopy(
            dict(
                (name, data[1]) for (name, data) in self._streams.iteritems()))

    def getStreamsOfType(self, data_type):
        """
		Returns all streams of a specific type

		:param data_type: Requested data type
		:type data_type: ``qidata.DataType``
		:return: Every stream of the requested type known by the data set
		:rtype: dict
		"""
        return copy.deepcopy(
            dict((name, data[1]) for (name, data) in self._streams.iteritems()
                 if data[0] == data_type))

    def getStream(self, stream_name):
        """
		Returns the requested stream

		:param stream_name: Requested data stream
		:type stream_name: str
		:return: The requested stream
		:rtype: dict
		:raises: KeyError if stream_name does not exist
		"""
        return copy.deepcopy(self._streams[stream_name][1])

    def getStreamType(self, stream_name):
        """
		Returns the type of a specific stream

		:param stream_name: Data stream of interest
		:type stream_name: str
		:return: The requested stream's data type
		:rtype: ``qidata.DataType``
		"""
        return self._streams[stream_name][0]

    def addToStream(self, stream_name, file_timestamp_pair_to_add):
        """
		Add a pair (timestamp, file name) to a data stream

		:param stream_name: Name of the stream to modify
		:type stream_name: str
		:param file_timestamp_pair_to_add: Pair of filename and timestamp
		:type file_timestamp_pair_to_add: tuple
		:raises: KeyError if stream does not exist
		:raises: ValueError if file is not in the dataset
		"""
        _tmp = file_timestamp_pair_to_add
        if not _tmp[1] in self.children:
            raise ValueError("Given file is not in the dataset")
        self._streams[stream_name][1][_tmp[0]] = _tmp[1]

    def removeFromStream(self, stream_name, file_to_remove):
        """
		Remove a file from a data stream

		:param stream_name: Name of the stream to modify
		:type stream_name: str
		:param file_to_remove: Name of the file to remove
		:type file_to_remove: str
		:raises: KeyError if stream does not exist
		:raises: ValueError if file is not in the stream
		"""
        for (ts, filename) in self._streams[stream_name][1].iteritems():
            if filename == file_to_remove:
                self._streams[stream_name][1].pop(ts)
                break
        else:
            raise ValueError("Given file is not in the stream")
        # si le stream devient vide, on devrait le supprimer

    @throwIfReadOnly
    def createNewFrame(self, *files):
        """
		Creates a new association of files in a :class:``QiDataFrame``

		:param files: files to include in the frame
		:param files: str
		:return: Created frame
		:rtype: :class:``QiDataFrame``
		:raises: TypeError if not enough files are given
		"""
        if len(files) < 2:
            raise TypeError(
                "createNewFrame needs at least 2 files (%d given)" %
                len(files))
        frame = qidataframe.QiDataFrame.create(files, self._folder_path)
        self._frames.append(frame)
        return frame

    @throwIfReadOnly
    def removeFrame(self, *files):
        """
		Remove a frame from the dataset

		:param files: Files composing the frame to remove.

		..note::
			A frame cannot be composed of only one file. So if only one file is
			given as argument, it is considered to be directly the frame to
			remove.
		"""
        if len(files) == 1:
            f = files[0]
        else:
            f = self.getFrame(*files)
        if f is None:
            return
        try:
            self._frames.remove(f)
        except ValueError:
            pass
        else:
            f.close()
            f._is_valid = False
            os.remove(f._file_path)

    def getAllFrames(self):
        """
		Returns all created frames

		:return: Every frames of the dataset
		:rtype: list
		"""
        return copy.copy(self._frames)

    def getFrame(self, *files):
        """
		Get an already created frame

		:param files: files composing the researched frame
		:param files: str
		:return: Researched frame
		:rtype: :class:``QiDataFrame``
		:raises: IndexError if no frame matches the requested files
		"""
        try:
            return [f for f in self._frames if set(files) == f._files][0]
        except IndexError:
            return None

    def openChild(self, name):
        """
		Open QiDataFile contained here

		:param name: Name of the file or folder to open
		:type name: str

		.. note::
			The opening mode used to open children is the opening mode of the
			QiDataSet itself
		"""
        path = os.path.join(self._folder_path, name)
        if not name in self.children:
            raise IOError("%s is not a child of the current dataset" % name)
        if os.path.isfile(path):
            return qidata.open(path, self.mode)
        # elif os.path.isdir(path):
        # 	return QiDataSet(path, self.mode)
        else:
            raise IOError("%s is neither a file nor a folder" % name)

    def setAnnotationStatus(self, annotator_name, metadata_type, is_total):
        """
		Set an annotation's status

		:param annotator_name: The annotator who made the full annotation
		:type annotator_name: str
		:param metadata_type: The annotation type that was fully annotated
		:type metadata_type: str
		:param is_total: True if the annotation is covering the whole dataset
		:type is_total: bool

		.. warning::

			Annotations CANNOT and MUST NOT be declared "Total" automatically.
			The value of such a statement can only be guaranteed if it emanates
			from a human.
		"""
        self._annotation_content[(annotator_name,metadata_type)] = \
          QiDataSet.AnnotationStatus.TOTAL\
           if is_total else QiDataSet.AnnotationStatus.PARTIAL

    # ───────────
    # Private API

    def _open(self):
        """
		Open the data set
		"""
        frames = glob.glob(self._folder_path + "/*.frame.xmp")
        for frame in frames:
            self._frames.append(qidataframe.QiDataFrame(frame, self.mode))
        self._xmp_file.__enter__()
        self._is_closed = False

        # Load content info stored in metadata
        _raw_metadata = self._xmp_file.metadata[QIDATA_CONTENT_NS]
        if _raw_metadata.children:
            data = _raw_metadata.value
            xmp_tools._removePrefixes(data)
            self._annotation_content = dict()
            if data.has_key("annotation_content"):
                content = data["annotation_content"]
                for annotator in content:
                    for annot_type, value in content[annotator].iteritems():
                        value = QiDataSet.AnnotationStatus[value]
                        self._annotation_content[(annotator,
                                                  annot_type)] = value

            if data.has_key("context"):
                self._context = Context(**data["context"])
            else:
                self._context = Context()

            for file_type, file_list in data["files_type"].iteritems():
                self._files_type[file_type] = file_list

        # 	# In a previous version, files_info was counting the number of file
        # 	# of each type. In the current version, we store for each type the
        # 	# list of all the corresponding files. This allows users to define
        # 	# more specific types than those which can be infered from the file
        # 	# extension.
        # 	# So if files_info is a string and not a list, it means it is an
        # 	# "old" version, therefore we need to rework it.
        # 	if len(data["files_info"])>0\
        # 	   and isinstance(data["files_info"].values()[0], basestring):
        # 		# Current file info is wrong
        # 		# Examine content to get proper file info
        # 		files_info = dict()
        # 		supported_subpaths = self.children
        # 		for path in supported_subpaths:
        # 			if isDataset(path):
        # 				# Avoid it for the moment
        # 				# But later we will have to handle sub-datasets
        # 				continue
        # 			file_type = qidatafile.getFileDataType(path)
        # 			if not files_info.has_key(str(file_type)):
        # 				files_info[str(file_type)] = []
        # 			files_info[str(file_type)].append(path)
        # 		self._content._type_content = dict(files_info)

        # If streams are defined, load them. We have to go through the
        # whole structure to make sure that timestamps are properly
        # converted to float (after removal of the appended prefix letter
        # and filenames must be converted to string, as this is the type we
        # use, but they are saved as unicode)
            if data.has_key("streams") and len(data["streams"]) > 0:
                for stream_name, stream in data["streams"].iteritems():
                    self._streams[stream_name] = [DataType[stream[0]], dict()]
                    for (timestamp, filename) in stream[1].iteritems():
                        _ts = tuple(map(int, timestamp[1:].split(".")))
                        self._streams[stream_name][1][_ts] = str(filename)

        else:
            # if no content info was stored, infere it from the files
            self._context = Context()
            self.examineContent()
        return self

    # ───────────────
    # Context Manager

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.close()

    # ──────────────
    # Textualization

    def __str__(self):
        return unicode(self).encode(encoding="utf-8")

    def __unicode__(self):
        # Path
        res_str = ""
        res_str += "Dataset path: " + self.name + "\n"

        # Types
        _da = [str(i) for i in self.datatypes_available]
        _da.sort()
        res_str += "Available types: " + textualize_sequence(_da,
                                                             unicode) + "\n"

        # Streams
        _sn = self._streams.keys()
        _sn.sort()
        print _sn
        _s = OrderedDict(
                          [(name, "%d files"%len(self._streams[name][1]))\
                              for name in _sn]
                        )
        res_str += "Available streams: " + textualize_mapping(_s,
                                                              unicode) + "\n"

        # Frames
        res_str += "Defined frames: %d\n" % len(self._frames)

        # Context
        res_str += "Context: " + unicode(self.context) + "\n"

        # Annotations
        res_str += "Available annotations: " + textualize_sequence(
            self.annotations_available, unicode) + "\n"
        return res_str
Beispiel #11
0
class QiDataFile(QiDataObject):

    # ───────────
    # Constructor

    def __init__(self, file_path, mode="r"):
        """
		Create and open a QiDataFile.
		QiDataFile wraps the xmp library specifically to store QiDataObjects under the
		QiData namespace.

		:param file_path: path to the file to open
		:type file_path: str
		:param mode: opening mode, "r" for reading, "w" for writing
		:param mode: str

		.. warnings::
			The mode behavior is different from the regular Python file mode.
			The file is NEVER created if it does not exist. Besides, opening
			an existing file in "w" mode does not overwrite it.
		"""
        if os.path.splitext(file_path)[1] == ".xmp":
            # If file is a .xmp, just read it normally
            # If the filename without xmp extension is an existing
            # file, then mark it as the real file opened
            xmp_path = file_path
            if os.path.exists(os.path.splitext(file_path)[0]):
                file_path = os.path.splitext(file_path)[0]

        elif os.path.exists(file_path + ".xmp"):
            # If there is an external annotation file, use it
            xmp_path = file_path + ".xmp"

        elif mode == "w":
            # If there is no external annotation file but we are in "w" mode
            # Copy the internal annotations in an external annotation file
            xmp_path = file_path + ".xmp"
            with XMPFile(file_path, rw=False) as _internal:
                with XMPFile(xmp_path, rw=True) as _external:
                    _external.libxmp_metadata = _internal.libxmp_metadata
        else:
            # Open the internal annotations
            xmp_path = file_path

        # Store the file path
        self._file_path = file_path

        # And prepare the xmp file
        self._xmp_file = XMPFile(xmp_path, rw=(mode == "w"))
        self._is_closed = True
        self._open()

    # ──────────
    # Properties

    @property
    def closed(self):
        """
		True if the file is closed
		"""
        return self._is_closed

    @property
    def mode(self):
        """
		Specify the file mode

		"r" => read-only mode
		"w" => read/write mode
		"""
        return "w" if self._xmp_file.rw else "r"

    @property
    def read_only(self):
        return ("r" == self.mode)

    @property
    def name(self):
        """
		Give the file path
		"""
        return self._file_path

    # ──────────
    # Public API

    def close(self):
        """
		Closes the file after writing the metadata
		"""
        if self.mode != "r":
            xmp_tools._save_annotations(self._xmp_file, self.annotations)
        self._xmp_file.close()
        self._is_closed = True

    @throwIfClosed
    def cancelChanges(self):
        """
		Erase changes by reloading metadata from file
		"""
        # Re-load annotations
        self._loadAnnotations()

    @throwIfClosed
    def addAnnotation(self, annotator, annotation, location=None):
        QiDataObject.addAnnotation(self, annotator, annotation, location)

    @throwIfClosed
    def removeAnnotation(self, annotator, annotation, location=None):
        QiDataObject.removeAnnotation(self, annotator, annotation, location)

    # ───────────
    # Private API

    def _open(self):
        """
		Open the file
		"""
        # file.__init__(self, self._file_path, "r")
        self._xmp_file.__enter__()
        self._is_closed = False
        self._loadAnnotations()
        return self

    @throwIfClosed
    def _loadAnnotations(self):
        """
		Loads annotations
		"""
        # Load annotations
        self._annotations = xmp_tools._load_annotations(self._xmp_file)

    # ───────────────
    # Context Manager

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.close()

    # ──────────────
    # Textualization

    def __unicode__(self):
        res_str = ""
        res_str += "File name: " + self.name + "\n"
        res_str += QiDataObject.__unicode__(self)
        return res_str
Beispiel #12
0
	def test_contextmanager_noop(self):
		original_sha1 = sha1(self.jpg_path)
		with XMPFile(self.jpg_path):
			pass
		noop_sha1 = sha1(self.jpg_path)
		self.assertEqual(original_sha1, noop_sha1)
Beispiel #13
0
	def test_sidecar_creation(self):
		with XMPFile(self.sidecar_only_path, rw=True) as sidecar:
			sidecar.metadata[TEST_NS].structure = "value"
		self.assertTrue(os.path.exists(self.sidecar_only_path))
		with XMPFile(self.sidecar_only_path, rw=True) as sidecar:
			self.assertEqual(sidecar.metadata[TEST_NS].structure.value, "value")
Beispiel #14
0
	def test_virtual_element_descriptor_set(self):
		with XMPFile(fixtures.sandboxedData(fixtures.JPG_PHOTO), rw = True) as xmpitem:
			xmpitem.metadata[TEST_NS].inexistent_attribute = 12
			self.assertIsInstance(xmpitem.metadata[TEST_NS].inexistent_attribute, XMPValue)
			self.assertEqual(xmpitem.metadata[TEST_NS].inexistent_attribute.value, "12")
Beispiel #15
0
	def setUp(self):
		self.xmp_file = XMPFile(fixtures.sandboxedData(fixtures.JPG_PHOTO))
		self.xmp_file.open()
		self.xmp_metadata = self.xmp_file.metadata[TEST_NS]
Beispiel #16
0
	def test_xmp_file_creation(self):
		with XMPFile(self.xmp_extension_path, rw=True) as xmp_file:
			xmp_file.metadata[TEST_NS].structure = "value"
		self.assertTrue(os.path.exists(self.xmp_extension_path))
		with XMPFile(self.xmp_extension_path) as xmp_file:
			self.assertEqual(xmp_file.metadata[TEST_NS].structure.value, "value")
Beispiel #17
0
	def setUp(self):
		self.EXPECTED_NS_UIDS = fixtures.JPG_PHOTO_NS_UIDS

		self.xmp_file = XMPFile(fixtures.sandboxedData(fixtures.JPG_PHOTO))
		self.xmp_file.__enter__()
		self.example_xmp = self.xmp_file.metadata