Commit 5f2e5980 authored by Manuel Günther's avatar Manuel Günther
Browse files

New README and documentation strategy; changed purpose 'enrol' to 'enroll'

parent 2a08ecc8
*~
*.swp
*.pyc
*.so
*.dylib
bin
eggs
parts
.installed.cfg
.mr.developer.cfg
*.egg-info
src
develop-eggs
built-docs
dist
sphinx
dist
.nfs*
.gdb_history
build
*.egg
src/
db.sql3
......@@ -6,6 +6,7 @@ matrix:
env:
- secure: lOs+/EAfuj7ISmSdp5d4J06375GNzL08LvwFJDFhbynjK+ld+Gtr+NX4qvcnA5CCPBT8fdIWrqynPKv3gfQBAWzTh2WvmbUuFgZ1ZMJKV1FxCK5RqFxinYjM6I7wpknPdBVxIr4HtDdfk7xsu+8lotcfYRaI0/JLa5E5xLU+YB0=
- secure: XVEXnr4kcd6s+5Fd+g6A3m18ApFvgmT2LH51HHli4FlGoBuHm4C50sIxMh2tDDpfxxkGjo64AHVb+nHQgTZPYyfg5hXsRiIWyT0tVwvE1EiX5x5WINwgiV+/VMT/lKkkkUD2A4stX7Mjlkvz4UTshXdz9JTg0/aKXPRYEsdS3mA=
- BOB_DOCUMENTATION_SERVER=https://www.idiap.ch/software/bob/docs/latest/bioidiap/%s/master
- python: 3.2
env:
- NUMPYSPEC===1.8.0
......@@ -15,7 +16,7 @@ matrix:
before_install:
- sudo add-apt-repository -y ppa:biometrics/bob
- sudo apt-get update -qq
- sudo apt-get install -qq --force-yes libboost-all-dev libblitz1-dev libhdf5-serial-dev libmatio-dev libatlas-dev libatlas-base-dev liblapack-dev
- sudo apt-get install -qq --force-yes libboost-all-dev libblitz1-dev libhdf5-serial-dev libmatio-dev libatlas-dev libatlas-base-dev liblapack-dev texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended
- if [ -n "${NUMPYSPEC}" ]; then sudo apt-get install -qq gfortran; fi
- if [ -n "${NUMPYSPEC}" ]; then pip install --upgrade pip setuptools; fi
- if [ -n "${NUMPYSPEC}" ]; then pip install --find-links http://wheels.astropy.org/ --find-links http://wheels2.astropy.org/ --use-wheel numpy$NUMPYSPEC; fi
......
========================
YouTube Faces Database
========================
.. vim: set fileencoding=utf-8 :
.. Manuel Guenther <manuel.guenther@idiap.ch>
.. Fri Oct 31 14:18:57 CET 2014
.. image:: http://img.shields.io/badge/docs-stable-yellow.png
:target: http://pythonhosted.org/bob.db.youtube/index.html
.. image:: http://img.shields.io/badge/docs-latest-orange.png
:target: https://www.idiap.ch/software/bob/docs/latest/bioidiap/bob.db.youtube/master/index.html
.. image:: https://travis-ci.org/bioidiap/bob.db.youtube.svg?branch=master
:target: https://travis-ci.org/bioidiap/bob.db.youtube
.. image:: https://coveralls.io/repos/bioidiap/bob.db.youtube/badge.png
:target: https://coveralls.io/r/bioidiap/bob.db.youtube
.. image:: https://img.shields.io/badge/github-master-0000c0.png
:target: https://github.com/bioidiap/bob.db.youtube/tree/master
.. image:: http://img.shields.io/pypi/v/bob.db.youtube.png
:target: https://pypi.python.org/pypi/bob.db.youtube
.. image:: http://img.shields.io/pypi/dm/bob.db.youtube.png
:target: https://pypi.python.org/pypi/bob.db.youtube
.. image:: https://img.shields.io/badge/original-data--files-a000a0.png
:target: http://www.cs.tau.ac.il/~wolf/ytfaces
==========================================
YouTube Faces Database Interface for Bob
==========================================
This package contains an interface for the evaluation protocol of the `YouTube Faces`_ database.
This package does not contain the original `YouTube Faces`_ data files, which need to be obtained through the link above.
Installation
------------
To install this package -- alone or together with other `Packages of Bob <https://github.com/idiap/bob/wiki/Packages>`_ -- please read the `Installation Instructions <https://github.com/idiap/bob/wiki/Installation>`_.
For Bob_ to be able to work properly, some dependent packages are required to be installed.
Please make sure that you have read the `Dependencies <https://github.com/idiap/bob/wiki/Dependencies>`_ for your operating system.
Documentation
-------------
For further documentation on this package, please read the `Stable Version <http://pythonhosted.org/bob.db.youtube/index.html>`_ or the `Latest Version <https://www.idiap.ch/software/bob/docs/latest/bioidiap/bob.db.youtube/master/index.html>`_ of the documentation.
For a list of tutorials on this or the other packages ob Bob_, or information on submitting issues, asking questions and starting discussions, please visit its website.
.. _bob: https://www.idiap.ch/software/bob
.. _youtube faces: http://www.cs.tau.ac.il/~wolf/ytfaces
This package contains the access API and descriptions for the `YouTube Faces Database <http://www.cs.tau.ac.il/~wolf/ytfaces/>`_.
The actual raw data for the database should be downloaded from the original URL.
This package only contains the `Bob <http://www.idiap.ch/software/bob/>`_ accessor methods to use the DB directly from python, with our certified protocols.
You would normally not install this package unless you are maintaining it.
What you would do instead is to tie it in at the package you need to **use** it.
There are a few ways to achieve this:
1. You can add this package as a requirement at the ``setup.py`` for your own `satellite package <https://github.com/idiap/bob/wiki/Virtual-Work-Environments-with-Buildout>`_ or to your Buildout ``.cfg`` file, if you prefer it that way.
With this method, this package gets automatically downloaded and installed on your working environment, or
2. You can manually download and install this package using commands like ``easy_install`` or ``pip``.
The package is available in two different distribution formats:
1. You can download it from `PyPI <http://pypi.python.org/pypi>`_, or
2. You can download it in its source form from `its git repository <https://github.com/bioidiap/bob.db.youtube>`_.
When you download the version at the git repository, you will need to run a command to recreate the backend SQLite file required for its operation.
You can mix and match points 1/2 and a/b above based on your requirements.
Here are some examples:
Modify your setup.py and download from PyPI
===========================================
That is the easiest.
Edit your ``setup.py`` in your satellite package and add the following entry in the ``install_requires`` section (note: ``...`` means `whatever extra stuff you may have in-between`, don't put that on your script)::
install_requires=[
...
"bob.db.youtube",
],
Proceed normally with your ``boostrap/buildout`` steps and you should be all set.
That means you can now import the ``bob.db.youtube`` namespace into your scripts.
Modify your buildout.cfg and download from git
==============================================
You will need to add a dependence to `mr.developer <http://pypi.python.org/pypi/mr.developer/>`_ to be able to install from our git repositories.
Your ``buildout.cfg`` file should contain the following lines::
[buildout]
...
extensions = mr.developer
auto-checkout = *
eggs = ...
bob.db.youtube
[sources]
bob.db.youtube = git https://github.com/bioidiap/bob.db.youtube.git
...
......@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""This script creates the Labeled Faces in the Wild (LFW) database in a single pass.
"""This script creates the YouTube Faces database in a single pass.
"""
import os
......
......@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Commands the Labeled Faces in the Wild database can respond to.
"""Commands the YouTube Faces database can respond to.
"""
import os
......@@ -114,7 +114,11 @@ def annotations(args):
from bob.db.base.utils import null
output = null()
a = db.annotations(args.id)
f = db.files([args.id])
if len(f) != 1:
output.write('Cannot find file with id "%d" in database\n' % args.id)
return 1
a = db.annotations(f[0])
k = sorted(a.keys(), key=lambda x : int(x.split('.')[1]))
for i in k:
output.write("\n%s "%i)
......
......@@ -104,5 +104,5 @@ class Pair(Base):
self.is_match = is_match
def __repr__(self):
return "<Pair('%s', '%s', '%s', '%d')>" % (self.protocol, self.enrol_directory_id, self.probe_directory_id, 1 if self.is_match else 0)
return "<Pair('%s', '%s', '%s', '%d')>" % (self.protocol, self.enroll_directory_id, self.probe_directory_id, 1 if self.is_match else 0)
......@@ -56,7 +56,7 @@ class Database(bob.db.verification.utils.SQLiteDatabase):
self.m_valid_protocols = ('fold1', 'fold2', 'fold3', 'fold4', 'fold5', 'fold6', 'fold7', 'fold8', 'fold9', 'fold10')
self.m_valid_groups = ('world', 'dev', 'eval')
self.m_valid_purposes = ('enrol', 'probe')
self.m_valid_purposes = ('enroll', 'probe')
self.m_valid_classes = ('client', 'impostor') # 'matched' and 'unmatched'
self.m_subworld_counts = {'onefolds':1, 'twofolds':2, 'threefolds':3, 'fourfolds':4, 'fivefolds':5, 'sixfolds':6, 'sevenfolds':7}
self.m_valid_types = ('restricted', 'unrestricted')
......@@ -315,7 +315,7 @@ class Database(bob.db.verification.utils.SQLiteDatabase):
The groups to which the objects belong ('world', 'dev', 'eval')
purposes
The purposes of the objects ('enrol', 'probe')
The purposes of the objects ('enroll', 'probe')
subworld
The subset of the training data. Has to be specified if groups includes 'world'
......@@ -366,7 +366,7 @@ class Database(bob.db.verification.utils.SQLiteDatabase):
if 'dev' in groups:
# development set of current fold of view 2
devset = self.__dev_for__(protocol)
if 'enrol' in purposes:
if 'enroll' in purposes:
queries.append(\
self.query(Directory).join((Pair, Directory.id == Pair.enroll_directory_id)).\
filter(Pair.protocol.in_(devset)))
......@@ -379,7 +379,7 @@ class Database(bob.db.verification.utils.SQLiteDatabase):
if 'eval' in groups:
# evaluation set of current fold of view 2; this is the REAL fold
if 'enrol' in purposes:
if 'enroll' in purposes:
queries.append(\
self.query(Directory).join((Pair, Directory.id == Pair.enroll_directory_id)).\
filter(Pair.protocol == protocol))
......@@ -426,7 +426,7 @@ class Database(bob.db.verification.utils.SQLiteDatabase):
Returns: A set of Directory objects with the given properties.
"""
return self.objects(self.__zt_fold_for__(protocol), groups='dev', model_ids = model_ids, purposes='enrol')
return self.objects(self.__zt_fold_for__(protocol), groups='dev', model_ids = model_ids, purposes='enroll')
def zobjects(self, protocol, model_ids=None, groups=None):
......@@ -510,14 +510,14 @@ class Database(bob.db.verification.utils.SQLiteDatabase):
return retval
def annotations(self, directory_id, image_names = None):
def annotations(self, directory, image_names = None):
"""Returns the annotations for the given file id as a dictionary of dictionaries, e.g. {'1.56.jpg' : {'topleft':(y,x), 'bottomright':(y,x)}, '1.57.jpg' : {'topleft':(y,x), 'bottomright':(y,x)}, ...}.
Here, the key of the dictionary is the full image file name of the original image.
Keyword parameters:
directory_id
The id of the directory for which you want to retrieve the annotations
directory
The :py:class:`Directory` object for which you want to retrieve the annotations
image_names
If given, only the annotations for the given image names (without path, but including filaname extension) are extracted and returned
......@@ -526,10 +526,7 @@ class Database(bob.db.verification.utils.SQLiteDatabase):
if self.original_directory is None:
raise ValueError("Please specify the 'original_directory' in the constructor of this class to get the annotations.")
query = self.query(Directory).filter(Directory.id == directory_id)
assert query.count() == 1
video = query.first()
annotation_file = os.path.join(self.original_directory, video.client.name + self.annotation_extension)
annotation_file = os.path.join(self.original_directory, directory.client.name + self.annotation_extension)
annots = {}
......@@ -538,7 +535,7 @@ class Database(bob.db.verification.utils.SQLiteDatabase):
splits = line.rstrip().split(',')
shot_id = int(splits[0].split('\\')[1])
index = splits[0].split('\\')[2]
if shot_id == video.shot_id:
if shot_id == directory.shot_id:
if image_names is None or index in image_names:
# coordinates are: center x, center y, width, height
(center_y, center_x, d_y, d_x) = (float(splits[3]), float(splits[2]), float(splits[5])/2., float(splits[4])/2.)
......
......@@ -136,8 +136,8 @@ def test_objects():
# check that the files() function returns the same number of elements as the models() function does
for p,l in _expected_models.items():
assert len(db.objects(protocol=p, groups='dev', purposes='enrol')) == l[0]
assert len(db.objects(protocol=p, groups='eval', purposes='enrol')) == l[1]
assert len(db.objects(protocol=p, groups='dev', purposes='enroll')) == l[0]
assert len(db.objects(protocol=p, groups='eval', purposes='enroll')) == l[1]
# check the number of probe files is correct
for p,l in _expected_probes.items():
......@@ -184,8 +184,8 @@ def test_unrestricted():
for p,l in _expected_unrestricted_training_images.items():
assert len(db.objects(protocol=p, groups='world', world_type='unrestricted')) == l
# for dev and eval, restricted and unrestricted should return the same number of files
assert len(db.objects(protocol=p, groups='dev', purposes='enrol', world_type='unrestricted')) == _expected_models[p][0]
assert len(db.objects(protocol=p, groups='eval', purposes='enrol', world_type='unrestricted')) == _expected_models[p][1]
assert len(db.objects(protocol=p, groups='dev', purposes='enroll', world_type='unrestricted')) == _expected_models[p][0]
assert len(db.objects(protocol=p, groups='eval', purposes='enroll', world_type='unrestricted')) == _expected_models[p][1]
assert len(db.objects(protocol=p, groups='dev', purposes='probe', world_type='unrestricted')) == _expected_probes[p][0]
assert len(db.objects(protocol=p, groups='eval', purposes='probe', world_type='unrestricted')) == _expected_probes[p][1]
......@@ -226,13 +226,13 @@ def test_annotations():
import glob
images = db.original_file_name(dir)
# get the annotations for 10 images
annotations = db.annotations(dir.id)
annotations = db.annotations(dir)
# check that images and annotations are from the same image ID
assert len(images) == len(annotations)
# check a subset of the annotations
image_names = sorted(set([os.path.basename(images[random.randrange(len(images))]) for i in range(10)]))
annotations = db.annotations(dir.id, image_names = image_names)
annotations = db.annotations(dir, image_names = image_names)
assert len(annotations) <= 10
for i, image_id in enumerate(sorted(annotations.keys())):
assert image_id == image_names[i]
......@@ -246,7 +246,7 @@ def test_annotations():
def test_driver_api():
from bob.db.base.script.dbmanage import main
assert main('youtube dumplist --self-test'.split()) == 0
assert main('youtube dumplist --protocol=fold8 --group=dev --purpose=enrol --self-test'.split()) == 0
assert main('youtube dumplist --protocol=fold8 --group=dev --purpose=enroll --self-test'.split()) == 0
assert main('youtube dumppairs --self-test'.split()) == 0
assert main('youtube dumppairs --protocol=fold8 --group=dev --class=client --self-test'.split()) == 0
assert main('youtube checkfiles --self-test'.split()) == 0
......
......@@ -3,21 +3,12 @@
# Andre Anjos <andre.anjos@idiap.ch>
# Mon 13 Aug 2012 12:38:15 CEST
#
# Copyright (C) 2011-2012 Idiap Research Institute, Martigny, Switzerland
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Copyright (C) 2011-2014 Idiap Research Institute, Martigny, Switzerland
import sys, os
import os
import sys
import glob
import pkg_resources
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
......@@ -71,15 +62,17 @@ project = u'YouTube Faces Database (Bob API)'
import time
copyright = u'%s, Idiap Research Institute' % time.strftime('%Y')
# Grab the setup entry
distribution = pkg_resources.require('bob.db.youtube')[0]
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
from bob.db.youtube.driver import Interface
version = Interface().version()
version = distribution.version
# The full version, including alpha/beta/rc tags.
release = version
release = distribution.version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
......@@ -140,12 +133,12 @@ if sphinx.__version__ >= "1.0":
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = ''
html_logo = 'img/logo.png'
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = ''
html_favicon = 'img/favicon.ico'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
......@@ -194,7 +187,7 @@ html_favicon = ''
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'bobdbdoc'
htmlhelp_basename = 'bob_db_youtube_doc'
# -- Options for LaTeX output --------------------------------------------------
......@@ -208,7 +201,7 @@ latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'bobdbman.tex', u'Bob',
('index', 'bob_db_youtube.tex', u'Bob',
u'Biometrics Group, Idiap Research Institute', 'manual'),
]
......@@ -246,40 +239,15 @@ man_pages = [
('index', 'bob', u'Labeled Faces in the Wild Database (Bob API) Documentation', [u'Idiap Research Institute'], 1)
]
# We want to remove all private (i.e. _. or __.__) members
# that are not in the list of accepted functions
accepted_private_functions = ['__call__']
def member_function_test(app, what, name, obj, skip, options):
# test if we have a private function
if len(name) > 1 and name[0] == '_':
# test if this private function should be allowed
if name not in accepted_private_functions:
# omit privat functions that are not in the list of accepted private functions
return True
else:
# test if the method is documented
if not hasattr(obj, '__doc__') or not obj.__doc__:
return True
# Skips selected members in auto-generated documentation. Unfortunately, old
# versions of Boost.Python will not generate a __self__ member for static
# methods and that screws-up Sphinx processing.
if sphinx.__version__ < "1.0":
# We have to remove objects that do not have a __self__ attribute set
import types
if isinstance(obj, types.BuiltinFunctionType) and \
not hasattr(obj, '__self__') and what == 'class':
app.warn("Skipping %s %s (no __self__)" % (what, name))
return True
return False
# Default processing flags for sphinx
autoclass_content = 'both'
autodoc_member_order = 'bysource'
autodoc_default_flags = ['members', 'undoc-members', 'special-members', 'inherited-members', 'show-inheritance']
autodoc_default_flags = ['members', 'undoc-members', 'inherited-members', 'show-inheritance']
# For inter-documentation mapping:
from bob.extension.utils import link_documentation
intersphinx_mapping = link_documentation(['python', 'bob.db.base', 'bob.db.verification.utils'])
def setup(app):
app.connect('autodoc-skip-member', member_function_test)
pass
......@@ -13,12 +13,15 @@
User's Guide
==============
This database interface implements the default evaluation protocols as they are given on the `YouTube Faces Database web page <http://www.cs.tau.ac.il/~wolf/ytfaces>`_.
It implements the :py:mod:`bob.db.verifcation.utils` interface, so that it can be used like any other of our databases.
This package contains the access API and descriptions for the `YouTube Faces`_ database.
It only contains the Bob_ accessor methods to use the DB directly from python, with our certified protocols.
The actual raw data for the `YouTube Faces`_ database should be downloaded from the original URL (though we were not able to contact the corresponding Professor).
.. note::
This database interface does not include the original data.
To be able to run experiments on the YouTube Faces database, you need to get a copy of the original data from the above mentioned web page.
The Database Interface
----------------------
The :py:class:`bob.db.youtube.Database` complies with the standard biometric verification database as described in :ref:`commons`, implementing the interface :py:class:`bob.db.verification.utils.SQLiteDatabase`.
The Protocols
......@@ -43,7 +46,7 @@ For example, to get the list of supported protocols, you can query the list of s
>>> db.protocol_names()
('fold1', 'fold2', 'fold3', 'fold4', 'fold5', 'fold6', 'fold7', 'fold8', 'fold9', 'fold10')
These protocol names define the 10 different splits of the YouTube Faces protocol, for which experiments can be run.
These protocol names define the 10 different splits of the `YouTube Faces`_ protocol, for which experiments can be run.
Some of the remaining query functions require a protocol to be selected.
For each protocol, the splits of the database are distributed into three different groups: ``('world', 'dev', 'eval')``.
......@@ -61,13 +64,12 @@ For the final evaluation it is required that 10 different experiments are execut
Finally, the classification accuracy is reported as an average of the 10 classification results.
The Directory Objects
---------------------
The most important method of the interface is the :py:func:`bob.db.youtube.Database.objects` function.
You can use this function to query the `information` for the protocols.
For the YouTube database, the `information` consists of a list of :py:class:`bob.db.youtube.models.Directory`.
You can use this function to query the *information* for the protocols.
For the YouTube database, the information consists of a list of :py:class:`bob.db.youtube.models.Directory`.
Each ``Directory`` contains information about a video, such as the identity of the client, the shot id and the (relative) path of the directory in the database:
.. .. doctest::
......@@ -83,7 +85,7 @@ Each ``Directory`` contains information about a video, such as the identity of t
1
>>> d.shot_id
0
>>> d.path
>>> d.path #doctest:+SKIP
u'AJ_Cook/0'
These ``Directory`` objects can be used to get the path for the image data.
......@@ -97,8 +99,8 @@ Since the videos are stored as a list of frames, the ``Directory`` interface wil
[...]/AJ_Cook/0/0.123.jpg
.. warning::
Please note that -- in opposition to other bob.db database interfaces -- the ``original_file_name`` function returns a **list** of file names.
Likewise, ``original_file_names`` returns a list of lists of file names.
Please note that -- in opposition to most other bob.db database interfaces -- the :py:meth:`bob.db.youtube.Database.original_file_name` function returns a **list** of file names.
Likewise, :py:meth:`bob.db.youtube.Database.original_file_names` returns a list of lists of file names.
Finally, bounding boxes are annotated in the images.
......@@ -108,23 +110,26 @@ In the example below, the annotations for the first 20 images are read and retur
.. code-block:: python
>>> file_name_stems = [os.path.basename(f) for f in file_names[:20]]
>>> annotations = db.annotations(d.id, file_name_stems)
>>> annotations = db.annotations(d, file_name_stems)
>>> sorted(annotations.keys()) == file_name_stems
True
>>> bounding_box = annotations[file_name_stems[0]]
>>> print (bounding_box)
{'topleft': (56.0, 205.0), 'bottomright': (112.0, 261.0)}
The annotations for one image can, for example, be used to cut out the face region from the image:
The annotations for one image can, for example, be used to cut out the face region from the image, using default functionality from other Bob_ packages:
.. code-block:: python
>>> import bob.io.base
>>> import bob.io.image
>>> import bob.ip.color
>>> color_image = bob.io.load(file_names[0])
>>> color_image = bob.io.base.load(file_names[0])
>>> gray_image = bob.ip.color.rgb_to_gray(color_image)
>>> face_region = gray_image[bounding_box['topleft'][0] : bounding_box['bottomright'][0],
bounding_box['topleft'][1] : bounding_box['bottomright'][1]]
.. _bob: https://www.idiap.ch/software/bob
.. _youtube faces: http://www.cs.tau.ac.il/~wolf/ytfaces
......@@ -42,10 +42,10 @@ setup(
'bob.db': [
'youtube = bob.db.youtube.driver:Interface',
],
},
classifiers = [
'Framework :: Bob',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment