pax_global_header00006660000000000000000000000064144534646170014530gustar00rootroot0000000000000052 comment=5c2dc8d3af1e0af0290dcd7ae2cae92589f305a1 dataset-1.6.2/000077500000000000000000000000001445346461700131635ustar00rootroot00000000000000dataset-1.6.2/.bumpversion.cfg000066400000000000000000000004051445346461700162720ustar00rootroot00000000000000[bumpversion] current_version = 1.6.2 tag_name = {new_version} commit = True tag = True [bumpversion:file:setup.py] search = version="{current_version}" replace = version="{new_version}" [bumpversion:file:dataset/__init__.py] [bumpversion:file:docs/conf.py] dataset-1.6.2/.github/000077500000000000000000000000001445346461700145235ustar00rootroot00000000000000dataset-1.6.2/.github/FUNDING.yml000066400000000000000000000001221445346461700163330ustar00rootroot00000000000000# These are supported funding model platforms github: pudo open_collective: pudo dataset-1.6.2/.github/dependabot.yml000066400000000000000000000002201445346461700173450ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily time: "04:00" open-pull-requests-limit: 100 dataset-1.6.2/.github/workflows/000077500000000000000000000000001445346461700165605ustar00rootroot00000000000000dataset-1.6.2/.github/workflows/build.yml000066400000000000000000000040601445346461700204020ustar00rootroot00000000000000name: build on: [push] jobs: python: runs-on: ubuntu-latest services: postgres: image: postgres env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: dataset ports: - 5432/tcp options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 mysql: image: mysql env: MYSQL_USER: mysql MYSQL_PASSWORD: mysql MYSQL_DATABASE: dataset MYSQL_ROOT_PASSWORD: mysql ports: - 3306/tcp options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - uses: actions/checkout@v1 - name: Show ref run: | echo "$GITHUB_REF" - name: Set up Python uses: actions/setup-python@v1 with: python-version: "3.x" - name: Install dependencies env: DEBIAN_FRONTEND: noninteractive run: | sudo apt-get -qq update pip install -e ".[dev]" - name: Run SQLite tests env: DATABASE_URL: "sqlite:///:memory:" run: | make test - name: Run PostgreSQL tests env: DATABASE_URL: "postgresql://postgres:postgres@127.0.0.1:${{ job.services.postgres.ports[5432] }}/dataset" run: | make test - name: Run mysql tests env: DATABASE_URL: "mysql+pymysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/dataset?charset=utf8" run: | make test - name: Run flake8 to lint run: | flake8 --ignore=E501,E123,E124,E126,E127,E128 dataset - name: Build a distribution run: | python setup.py sdist bdist_wheel - name: Publish a Python distribution to PyPI if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.pypi_password }} dataset-1.6.2/.gitignore000066400000000000000000000002321445346461700151500ustar00rootroot00000000000000*.pyc *.egg-info *.egg .eggs/ dist/* .tox/* .vscode/* build/* .DS_Store .watchr .coverage htmlcov/ *.pyo env3/* env/* Test.yaml Freezefile.yaml :memory: dataset-1.6.2/CHANGELOG.md000066400000000000000000000040051445346461700147730ustar00rootroot00000000000000# dataset ChangeLog *The changelog has only been started with version 0.3.12, previous changes must be reconstructed from revision history.* * 1.2.0: Add support for views, multiple comparison operators. Remove support for Python 2. * 1.1.0: Introduce `types` system to shortcut for SQLA types. * 1.0.0: Massive re-factor and code cleanup. * 0.6.0: Remove sqlite_datetime_fix for automatic int-casting of dates, make table['foo', 'bar'] an alias for table.distinct('foo', 'bar'), check validity of column and table names more thoroughly, rename reflectMetadata constructor argument to reflect_metadata, fix ResultIter to not leave queries open (so you can update in a loop). * 0.5.7: dataset Databases can now have customized row types. This allows, for example, information to be retrieved in attribute-accessible dict subclasses, such as stuf. * 0.5.4: Context manager for transactions, thanks to @victorkashirin. * 0.5.1: Fix a regression where empty queries would raise an exception. * 0.5: Improve overall code quality and testing, including Travis CI. An advanced __getitem__ syntax which allowed for the specification of primary keys when getting a table was dropped. DDL is no longer run against a transaction, but the base connection. * 0.4: Python 3 support and switch to alembic for migrations. * 0.3.15: Fixes to update and insertion of data, thanks to @cli248 and @abhinav-upadhyay. * 0.3.14: dataset went viral somehow. Thanks to @gtsafas for refactorings, @alasdairnicol for fixing the Freezfile example in the documentation. @diegoguimaraes fixed the behaviour of insert to return the newly-created primary key ID. table.find_one() now returns a dict, not an SQLAlchemy ResultProxy. Slugs are now generated using the Python-Slugify package, removing slug code from dataset. * 0.3.13: Fixed logging, added support for transformations on result rows to support slug generation in output (#28). * 0.3.12: Makes table primary key's types and names configurable, fixing #19. Contributed by @dnatag. dataset-1.6.2/LICENSE.txt000066400000000000000000000021241445346461700150050ustar00rootroot00000000000000Copyright (c) 2013, Open Knowledge Foundation, Friedrich Lindenberg, Gregor Aisch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dataset-1.6.2/Makefile000066400000000000000000000005731445346461700146300ustar00rootroot00000000000000 all: clean test dists .PHONY: test test: pytest dists: python setup.py sdist python setup.py bdist_wheel release: dists pip install -q twine twine upload dist/* .PHONY: clean clean: rm -rf dist build .eggs find . -name '*.egg-info' -exec rm -fr {} + find . -name '*.egg' -exec rm -f {} + find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + dataset-1.6.2/README.md000066400000000000000000000011421445346461700144400ustar00rootroot00000000000000dataset: databases for lazy people ================================== ![build](https://github.com/pudo/dataset/workflows/build/badge.svg) In short, **dataset** makes reading and writing data in databases as simple as reading and writing JSON files. [Read the docs](https://dataset.readthedocs.io/) To install dataset, fetch it with ``pip``: ```bash $ pip install dataset ``` **Note:** as of version 1.0, **dataset** is split into two packages, with the data export features now extracted into a stand-alone package, **datafreeze**. See the relevant repository [here](https://github.com/pudo/datafreeze). dataset-1.6.2/dataset/000077500000000000000000000000001445346461700146105ustar00rootroot00000000000000dataset-1.6.2/dataset/__init__.py000066400000000000000000000043571445346461700167320ustar00rootroot00000000000000import os import warnings from dataset.database import Database from dataset.table import Table from dataset.util import row_type # shut up useless SA warning: warnings.filterwarnings("ignore", "Unicode type received non-unicode bind param value.") warnings.filterwarnings( "ignore", "Skipping unsupported ALTER for creation of implicit constraint" ) __all__ = ["Database", "Table", "connect"] __version__ = "1.6.2" def connect( url=None, schema=None, engine_kwargs=None, ensure_schema=True, row_type=row_type, sqlite_wal_mode=True, on_connect_statements=None, ): """Opens a new connection to a database. *url* can be any valid `SQLAlchemy engine URL`_. If *url* is not defined it will try to use *DATABASE_URL* from environment variable. Returns an instance of :py:class:`Database `. Additionally, *engine_kwargs* will be directly passed to SQLAlchemy, e.g. set *engine_kwargs={'pool_recycle': 3600}* will avoid `DB connection timeout`_. Set *row_type* to an alternate dict-like class to change the type of container rows are stored in.:: db = dataset.connect('sqlite:///factbook.db') One of the main features of `dataset` is to automatically create tables and columns as data is inserted. This behaviour can optionally be disabled via the `ensure_schema` argument. It can also be overridden in a lot of the data manipulation methods using the `ensure` flag. If you want to run custom SQLite pragmas on database connect, you can add them to on_connect_statements as a set of strings. You can view a full `list of PRAGMAs here`_. .. _SQLAlchemy Engine URL: http://docs.sqlalchemy.org/en/latest/core/engines.html#sqlalchemy.create_engine .. _DB connection timeout: http://docs.sqlalchemy.org/en/latest/core/pooling.html#setting-pool-recycle .. _list of PRAGMAs here: https://www.sqlite.org/pragma.html """ if url is None: url = os.environ.get("DATABASE_URL", "sqlite://") return Database( url, schema=schema, engine_kwargs=engine_kwargs, ensure_schema=ensure_schema, row_type=row_type, sqlite_wal_mode=sqlite_wal_mode, on_connect_statements=on_connect_statements, ) dataset-1.6.2/dataset/chunked.py000066400000000000000000000046651445346461700166160ustar00rootroot00000000000000import itertools class InvalidCallback(ValueError): pass class _Chunker(object): def __init__(self, table, chunksize, callback): self.queue = [] self.table = table self.chunksize = chunksize if callback and not callable(callback): raise InvalidCallback self.callback = callback def flush(self): self.queue.clear() def _queue_add(self, item): self.queue.append(item) if len(self.queue) >= self.chunksize: self.flush() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.flush() class ChunkedInsert(_Chunker): """Batch up insert operations with ChunkedInsert(my_table) as inserter: inserter(row) Rows will be inserted in groups of `chunksize` (defaulting to 1000). An optional callback can be provided that will be called before the insert. This callback takes one parameter which is the queue which is about to be inserted into the database """ def __init__(self, table, chunksize=1000, callback=None): self.fields = set() super().__init__(table, chunksize, callback) def insert(self, item): self.fields.update(item.keys()) super()._queue_add(item) def flush(self): for item in self.queue: for field in self.fields: item[field] = item.get(field) if self.callback is not None: self.callback(self.queue) self.table.insert_many(self.queue) super().flush() class ChunkedUpdate(_Chunker): """Batch up update operations with ChunkedUpdate(my_table) as updater: updater(row) Rows will be updated in groups of `chunksize` (defaulting to 1000). An optional callback can be provided that will be called before the update. This callback takes one parameter which is the queue which is about to be updated into the database """ def __init__(self, table, keys, chunksize=1000, callback=None): self.keys = keys super().__init__(table, chunksize, callback) def update(self, item): super()._queue_add(item) def flush(self): if self.callback is not None: self.callback(self.queue) self.queue.sort(key=dict.keys) for fields, items in itertools.groupby(self.queue, key=dict.keys): self.table.update_many(list(items), self.keys) super().flush() dataset-1.6.2/dataset/database.py000066400000000000000000000265531445346461700167410ustar00rootroot00000000000000import logging import threading from urllib.parse import parse_qs, urlparse from sqlalchemy import create_engine, inspect from sqlalchemy.sql import text from sqlalchemy.schema import MetaData from sqlalchemy.util import safe_reraise from sqlalchemy import event from alembic.migration import MigrationContext from alembic.operations import Operations from dataset.table import Table from dataset.util import ResultIter, row_type, safe_url, QUERY_STEP from dataset.util import normalize_table_name from dataset.types import Types log = logging.getLogger(__name__) class Database(object): """A database object represents a SQL database with multiple tables.""" def __init__( self, url, schema=None, engine_kwargs=None, ensure_schema=True, row_type=row_type, sqlite_wal_mode=True, on_connect_statements=None, ): """Configure and connect to the database.""" if engine_kwargs is None: engine_kwargs = {} parsed_url = urlparse(url) # if parsed_url.scheme.lower() in 'sqlite': # # ref: https://github.com/pudo/dataset/issues/163 # if 'poolclass' not in engine_kwargs: # engine_kwargs['poolclass'] = StaticPool self.lock = threading.RLock() self.local = threading.local() self.connections = {} if len(parsed_url.query): query = parse_qs(parsed_url.query) if schema is None: schema_qs = query.get("schema", query.get("searchpath", [])) if len(schema_qs): schema = schema_qs.pop() self.schema = schema self.engine = create_engine(url, **engine_kwargs) self.is_postgres = self.engine.dialect.name == "postgresql" self.is_sqlite = self.engine.dialect.name == "sqlite" if on_connect_statements is None: on_connect_statements = [] def _run_on_connect(dbapi_con, con_record): # reference: # https://stackoverflow.com/questions/9671490/how-to-set-sqlite-pragma-statements-with-sqlalchemy # https://stackoverflow.com/a/7831210/1890086 for statement in on_connect_statements: dbapi_con.execute(statement) if self.is_sqlite and parsed_url.path != "" and sqlite_wal_mode: # we only enable WAL mode for sqlite databases that are not in-memory on_connect_statements.append("PRAGMA journal_mode=WAL") if len(on_connect_statements): event.listen(self.engine, "connect", _run_on_connect) self.types = Types(is_postgres=self.is_postgres) self.url = url self.row_type = row_type self.ensure_schema = ensure_schema self._tables = {} @property def executable(self): """Connection against which statements will be executed.""" with self.lock: tid = threading.get_ident() if tid not in self.connections: self.connections[tid] = self.engine.connect() return self.connections[tid] @property def op(self): """Get an alembic operations context.""" ctx = MigrationContext.configure(self.executable) return Operations(ctx) @property def inspect(self): """Get a SQLAlchemy inspector.""" return inspect(self.executable) def has_table(self, name): return self.inspect.has_table(name, schema=self.schema) @property def metadata(self): """Return a SQLAlchemy schema cache object.""" return MetaData(schema=self.schema, bind=self.executable) @property def in_transaction(self): """Check if this database is in a transactional context.""" if not hasattr(self.local, "tx"): return False return len(self.local.tx) > 0 def _flush_tables(self): """Clear the table metadata after transaction rollbacks.""" for table in self._tables.values(): table._table = None def begin(self): """Enter a transaction explicitly. No data will be written until the transaction has been committed. """ if not hasattr(self.local, "tx"): self.local.tx = [] self.local.tx.append(self.executable.begin()) def commit(self): """Commit the current transaction. Make all statements executed since the transaction was begun permanent. """ if hasattr(self.local, "tx") and self.local.tx: tx = self.local.tx.pop() tx.commit() # Removed in 2020-12, I'm a bit worried this means that some DDL # operations in transactions won't cause metadata to refresh any # more: # self._flush_tables() def rollback(self): """Roll back the current transaction. Discard all statements executed since the transaction was begun. """ if hasattr(self.local, "tx") and self.local.tx: tx = self.local.tx.pop() tx.rollback() self._flush_tables() def __enter__(self): """Start a transaction.""" self.begin() return self def __exit__(self, error_type, error_value, traceback): """End a transaction by committing or rolling back.""" if error_type is None: try: self.commit() except Exception: with safe_reraise(): self.rollback() else: self.rollback() def close(self): """Close database connections. Makes this object unusable.""" with self.lock: for conn in self.connections.values(): conn.close() self.connections.clear() self.engine.dispose() self._tables = {} self.engine = None @property def tables(self): """Get a listing of all tables that exist in the database.""" return self.inspect.get_table_names(schema=self.schema) @property def views(self): """Get a listing of all views that exist in the database.""" return self.inspect.get_view_names(schema=self.schema) def __contains__(self, table_name): """Check if the given table name exists in the database.""" try: table_name = normalize_table_name(table_name) if table_name in self.tables: return True if table_name in self.views: return True return False except ValueError: return False def create_table( self, table_name, primary_id=None, primary_type=None, primary_increment=None ): """Create a new table. Either loads a table or creates it if it doesn't exist yet. You can define the name and type of the primary key field, if a new table is to be created. The default is to create an auto-incrementing integer, ``id``. You can also set the primary key to be a string or big integer. The caller will be responsible for the uniqueness of ``primary_id`` if it is defined as a text type. You can disable auto-increment behaviour for numeric primary keys by setting `primary_increment` to `False`. Returns a :py:class:`Table ` instance. :: table = db.create_table('population') # custom id and type table2 = db.create_table('population2', 'age') table3 = db.create_table('population3', primary_id='city', primary_type=db.types.text) # custom length of String table4 = db.create_table('population4', primary_id='city', primary_type=db.types.string(25)) # no primary key table5 = db.create_table('population5', primary_id=False) """ assert not isinstance( primary_type, str ), "Text-based primary_type support is dropped, use db.types." table_name = normalize_table_name(table_name) with self.lock: if table_name not in self._tables: self._tables[table_name] = Table( self, table_name, primary_id=primary_id, primary_type=primary_type, primary_increment=primary_increment, auto_create=True, ) return self._tables.get(table_name) def load_table(self, table_name): """Load a table. This will fail if the tables does not already exist in the database. If the table exists, its columns will be reflected and are available on the :py:class:`Table ` object. Returns a :py:class:`Table ` instance. :: table = db.load_table('population') """ table_name = normalize_table_name(table_name) with self.lock: if table_name not in self._tables: self._tables[table_name] = Table(self, table_name) return self._tables.get(table_name) def get_table( self, table_name, primary_id=None, primary_type=None, primary_increment=None, ): """Load or create a table. This is now the same as ``create_table``. :: table = db.get_table('population') # you can also use the short-hand syntax: table = db['population'] """ if not self.ensure_schema: return self.load_table(table_name) return self.create_table( table_name, primary_id, primary_type, primary_increment ) def __getitem__(self, table_name): """Get a given table.""" return self.get_table(table_name) def _ipython_key_completions_(self): """Completion for table names with IPython.""" return self.tables def query(self, query, *args, **kwargs): """Run a statement on the database directly. Allows for the execution of arbitrary read/write queries. A query can either be a plain text string, or a `SQLAlchemy expression `_. If a plain string is passed in, it will be converted to an expression automatically. Further positional and keyword arguments will be used for parameter binding. To include a positional argument in your query, use question marks in the query (i.e. ``SELECT * FROM tbl WHERE a = ?``). For keyword arguments, use a bind parameter (i.e. ``SELECT * FROM tbl WHERE a = :foo``). :: statement = 'SELECT user, COUNT(*) c FROM photos GROUP BY user' for row in db.query(statement): print(row['user'], row['c']) The returned iterator will yield each result sequentially. """ if isinstance(query, str): query = text(query) _step = kwargs.pop("_step", QUERY_STEP) if _step is False or _step == 0: _step = None rp = self.executable.execute(query, *args, **kwargs) return ResultIter(rp, row_type=self.row_type, step=_step) def __repr__(self): """Text representation contains the URL.""" return "" % safe_url(self.url) dataset-1.6.2/dataset/table.py000066400000000000000000000655641445346461700162710ustar00rootroot00000000000000import logging import warnings import threading from banal import ensure_list from sqlalchemy import func, select, false from sqlalchemy.sql import and_, expression from sqlalchemy.sql.expression import bindparam, ClauseElement from sqlalchemy.schema import Column, Index from sqlalchemy.schema import Table as SQLATable from sqlalchemy.exc import NoSuchTableError from dataset.types import Types, MYSQL_LENGTH_TYPES from dataset.util import index_name from dataset.util import DatasetException, ResultIter, QUERY_STEP from dataset.util import normalize_table_name, pad_chunk_columns from dataset.util import normalize_column_name, normalize_column_key log = logging.getLogger(__name__) class Table(object): """Represents a table in a database and exposes common operations.""" PRIMARY_DEFAULT = "id" def __init__( self, database, table_name, primary_id=None, primary_type=None, primary_increment=None, auto_create=False, ): """Initialise the table from database schema.""" self.db = database self.name = normalize_table_name(table_name) self._table = None self._columns = None self._indexes = [] self._primary_id = ( primary_id if primary_id is not None else self.PRIMARY_DEFAULT ) self._primary_type = primary_type if primary_type is not None else Types.integer if primary_increment is None: primary_increment = self._primary_type in (Types.integer, Types.bigint) self._primary_increment = primary_increment self._auto_create = auto_create @property def exists(self): """Check to see if the table currently exists in the database.""" if self._table is not None: return True return self.name in self.db @property def table(self): """Get a reference to the table, which may be reflected or created.""" if self._table is None: self._sync_table(()) return self._table @property def _column_keys(self): """Get a dictionary of all columns and their case mapping.""" if not self.exists: return {} with self.db.lock: if self._columns is None: # Initialise the table if it doesn't exist table = self.table self._columns = {} for column in table.columns: name = normalize_column_name(column.name) key = normalize_column_key(name) if key in self._columns: log.warning("Duplicate column: %s", name) self._columns[key] = name return self._columns @property def columns(self): """Get a listing of all columns that exist in the table.""" return list(self._column_keys.values()) def has_column(self, column): """Check if a column with the given name exists on this table.""" key = normalize_column_key(normalize_column_name(column)) return key in self._column_keys def _get_column_name(self, name): """Find the best column name with case-insensitive matching.""" name = normalize_column_name(name) key = normalize_column_key(name) return self._column_keys.get(key, name) def insert(self, row, ensure=None, types=None): """Add a ``row`` dict by inserting it into the table. If ``ensure`` is set, any of the keys of the row are not table columns, they will be created automatically. During column creation, ``types`` will be checked for a key matching the name of a column to be created, and the given SQLAlchemy column type will be used. Otherwise, the type is guessed from the row value, defaulting to a simple unicode field. :: data = dict(title='I am a banana!') table.insert(data) Returns the inserted row's primary key. """ row = self._sync_columns(row, ensure, types=types) res = self.db.executable.execute(self.table.insert(row)) if len(res.inserted_primary_key) > 0: return res.inserted_primary_key[0] return True def insert_ignore(self, row, keys, ensure=None, types=None): """Add a ``row`` dict into the table if the row does not exist. If rows with matching ``keys`` exist no change is made. Setting ``ensure`` results in automatically creating missing columns, i.e., keys of the row are not table columns. During column creation, ``types`` will be checked for a key matching the name of a column to be created, and the given SQLAlchemy column type will be used. Otherwise, the type is guessed from the row value, defaulting to a simple unicode field. :: data = dict(id=10, title='I am a banana!') table.insert_ignore(data, ['id']) """ row = self._sync_columns(row, ensure, types=types) if self._check_ensure(ensure): self.create_index(keys) args, _ = self._keys_to_args(row, keys) if self.count(**args) == 0: return self.insert(row, ensure=False) return False def insert_many(self, rows, chunk_size=1000, ensure=None, types=None): """Add many rows at a time. This is significantly faster than adding them one by one. Per default the rows are processed in chunks of 1000 per commit, unless you specify a different ``chunk_size``. See :py:meth:`insert() ` for details on the other parameters. :: rows = [dict(name='Dolly')] * 10000 table.insert_many(rows) """ # Sync table before inputting rows. sync_row = {} for row in rows: # Only get non-existing columns. sync_keys = list(sync_row.keys()) for key in [k for k in row.keys() if k not in sync_keys]: # Get a sample of the new column(s) from the row. sync_row[key] = row[key] self._sync_columns(sync_row, ensure, types=types) # Get columns name list to be used for padding later. columns = sync_row.keys() chunk = [] for index, row in enumerate(rows): chunk.append(row) # Insert when chunk_size is fulfilled or this is the last row if len(chunk) == chunk_size or index == len(rows) - 1: chunk = pad_chunk_columns(chunk, columns) self.table.insert().execute(chunk) chunk = [] def update(self, row, keys, ensure=None, types=None, return_count=False): """Update a row in the table. The update is managed via the set of column names stated in ``keys``: they will be used as filters for the data to be updated, using the values in ``row``. :: # update all entries with id matching 10, setting their title # columns data = dict(id=10, title='I am a banana!') table.update(data, ['id']) If keys in ``row`` update columns not present in the table, they will be created based on the settings of ``ensure`` and ``types``, matching the behavior of :py:meth:`insert() `. """ row = self._sync_columns(row, ensure, types=types) args, row = self._keys_to_args(row, keys) clause = self._args_to_clause(args) if not len(row): return self.count(clause) stmt = self.table.update(whereclause=clause, values=row) rp = self.db.executable.execute(stmt) if rp.supports_sane_rowcount(): return rp.rowcount if return_count: return self.count(clause) def update_many(self, rows, keys, chunk_size=1000, ensure=None, types=None): """Update many rows in the table at a time. This is significantly faster than updating them one by one. Per default the rows are processed in chunks of 1000 per commit, unless you specify a different ``chunk_size``. See :py:meth:`update() ` for details on the other parameters. """ keys = ensure_list(keys) chunk = [] columns = [] for index, row in enumerate(rows): columns.extend( col for col in row.keys() if (col not in columns) and (col not in keys) ) # bindparam requires names to not conflict (cannot be "id" for id) for key in keys: row["_%s" % key] = row[key] row.pop(key) chunk.append(row) # Update when chunk_size is fulfilled or this is the last row if len(chunk) == chunk_size or index == len(rows) - 1: cl = [self.table.c[k] == bindparam("_%s" % k) for k in keys] stmt = self.table.update( whereclause=and_(True, *cl), values={col: bindparam(col, required=False) for col in columns}, ) self.db.executable.execute(stmt, chunk) chunk = [] def upsert(self, row, keys, ensure=None, types=None): """An UPSERT is a smart combination of insert and update. If rows with matching ``keys`` exist they will be updated, otherwise a new row is inserted in the table. :: data = dict(id=10, title='I am a banana!') table.upsert(data, ['id']) """ row = self._sync_columns(row, ensure, types=types) if self._check_ensure(ensure): self.create_index(keys) row_count = self.update(row, keys, ensure=False, return_count=True) if row_count == 0: return self.insert(row, ensure=False) return True def upsert_many(self, rows, keys, chunk_size=1000, ensure=None, types=None): """ Sorts multiple input rows into upserts and inserts. Inserts are passed to insert and upserts are updated. See :py:meth:`upsert() ` and :py:meth:`insert_many() `. """ # Removing a bulk implementation in 5e09aba401. Doing this one by one # is incredibly slow, but doesn't run into issues with column creation. for row in rows: self.upsert(row, keys, ensure=ensure, types=types) def delete(self, *clauses, **filters): """Delete rows from the table. Keyword arguments can be used to add column-based filters. The filter criterion will always be equality: :: table.delete(place='Berlin') If no arguments are given, all records are deleted. """ if not self.exists: return False clause = self._args_to_clause(filters, clauses=clauses) stmt = self.table.delete(whereclause=clause) rp = self.db.executable.execute(stmt) return rp.rowcount > 0 def _reflect_table(self): """Load the tables definition from the database.""" with self.db.lock: self._columns = None try: self._table = SQLATable( self.name, self.db.metadata, schema=self.db.schema, autoload=True ) except NoSuchTableError: self._table = None def _threading_warn(self): if self.db.in_transaction and threading.active_count() > 1: warnings.warn( "Changing the database schema inside a transaction " "in a multi-threaded environment is likely to lead " "to race conditions and synchronization issues.", RuntimeWarning, ) def _sync_table(self, columns): """Lazy load, create or adapt the table structure in the database.""" if self._table is None: # Load an existing table from the database. self._reflect_table() if self._table is None: # Create the table with an initial set of columns. if not self._auto_create: raise DatasetException("Table does not exist: %s" % self.name) # Keep the lock scope small because this is run very often. with self.db.lock: self._threading_warn() self._table = SQLATable( self.name, self.db.metadata, schema=self.db.schema ) if self._primary_id is not False: # This can go wrong on DBMS like MySQL and SQLite where # tables cannot have no columns. column = Column( self._primary_id, self._primary_type, primary_key=True, autoincrement=self._primary_increment, ) self._table.append_column(column) for column in columns: if not column.name == self._primary_id: self._table.append_column(column) self._table.create(self.db.executable, checkfirst=True) self._columns = None elif len(columns): with self.db.lock: self._reflect_table() self._threading_warn() for column in columns: if not self.has_column(column.name): self.db.op.add_column(self.name, column, schema=self.db.schema) self._reflect_table() def _sync_columns(self, row, ensure, types=None): """Create missing columns (or the table) prior to writes. If automatic schema generation is disabled (``ensure`` is ``False``), this will remove any keys from the ``row`` for which there is no matching column. """ ensure = self._check_ensure(ensure) types = types or {} types = {self._get_column_name(k): v for (k, v) in types.items()} out = {} sync_columns = {} for name, value in row.items(): name = self._get_column_name(name) if self.has_column(name): out[name] = value elif ensure: _type = types.get(name) if _type is None: _type = self.db.types.guess(value) sync_columns[name] = Column(name, _type) out[name] = value self._sync_table(sync_columns.values()) return out def _check_ensure(self, ensure): if ensure is None: return self.db.ensure_schema return ensure def _generate_clause(self, column, op, value): if op in ("like",): return self.table.c[column].like(value) if op in ("ilike",): return self.table.c[column].ilike(value) if op in ("notlike",): return self.table.c[column].notlike(value) if op in ("notilike",): return self.table.c[column].notilike(value) if op in (">", "gt"): return self.table.c[column] > value if op in ("<", "lt"): return self.table.c[column] < value if op in (">=", "gte"): return self.table.c[column] >= value if op in ("<=", "lte"): return self.table.c[column] <= value if op in ("=", "==", "is"): return self.table.c[column] == value if op in ("!=", "<>", "not"): return self.table.c[column] != value if op in ("in",): return self.table.c[column].in_(value) if op in ("notin",): return self.table.c[column].notin_(value) if op in ("between", ".."): start, end = value return self.table.c[column].between(start, end) if op in ("startswith",): return self.table.c[column].like(value + "%") if op in ("endswith",): return self.table.c[column].like("%" + value) return false() def _args_to_clause(self, args, clauses=()): clauses = list(clauses) for column, value in args.items(): column = self._get_column_name(column) if not self.has_column(column): clauses.append(false()) elif isinstance(value, (list, tuple, set)): clauses.append(self._generate_clause(column, "in", value)) elif isinstance(value, dict): for op, op_value in value.items(): clauses.append(self._generate_clause(column, op, op_value)) else: clauses.append(self._generate_clause(column, "=", value)) return and_(True, *clauses) def _args_to_order_by(self, order_by): orderings = [] for ordering in ensure_list(order_by): if ordering is None: continue column = ordering.lstrip("-") column = self._get_column_name(column) if not self.has_column(column): continue if ordering.startswith("-"): orderings.append(self.table.c[column].desc()) else: orderings.append(self.table.c[column].asc()) return orderings def _keys_to_args(self, row, keys): keys = [self._get_column_name(k) for k in ensure_list(keys)] row = row.copy() args = {k: row.pop(k, None) for k in keys} return args, row def create_column(self, name, type, **kwargs): """Create a new column ``name`` of a specified type. :: table.create_column('created_at', db.types.datetime) `type` corresponds to an SQLAlchemy type as described by `dataset.db.Types`. Additional keyword arguments are passed to the constructor of `Column`, so that default values, and options like `nullable` and `unique` can be set. :: table.create_column('key', unique=True, nullable=False) table.create_column('food', default='banana') """ name = self._get_column_name(name) if self.has_column(name): log.debug("Column exists: %s" % name) return self._sync_table((Column(name, type, **kwargs),)) def create_column_by_example(self, name, value): """ Explicitly create a new column ``name`` with a type that is appropriate to store the given example ``value``. The type is guessed in the same way as for the insert method with ``ensure=True``. :: table.create_column_by_example('length', 4.2) If a column of the same name already exists, no action is taken, even if it is not of the type we would have created. """ type_ = self.db.types.guess(value) self.create_column(name, type_) def drop_column(self, name): """ Drop the column ``name``. :: table.drop_column('created_at') """ if self.db.engine.dialect.name == "sqlite": raise RuntimeError("SQLite does not support dropping columns.") name = self._get_column_name(name) with self.db.lock: if not self.exists or not self.has_column(name): log.debug("Column does not exist: %s", name) return self._threading_warn() self.db.op.drop_column(self.table.name, name, schema=self.table.schema) self._reflect_table() def drop(self): """Drop the table from the database. Deletes both the schema and all the contents within it. """ with self.db.lock: if self.exists: self._threading_warn() self.table.drop(self.db.executable, checkfirst=True) self._table = None self._columns = None self.db._tables.pop(self.name, None) def has_index(self, columns): """Check if an index exists to cover the given ``columns``.""" if not self.exists: return False columns = set([self._get_column_name(c) for c in ensure_list(columns)]) if columns in self._indexes: return True for column in columns: if not self.has_column(column): return False indexes = self.db.inspect.get_indexes(self.name, schema=self.db.schema) for index in indexes: idx_columns = index.get("column_names", []) if len(columns.intersection(idx_columns)) == len(columns): self._indexes.append(columns) return True if self.table.primary_key is not None: pk_columns = [c.name for c in self.table.primary_key.columns] if len(columns.intersection(pk_columns)) == len(columns): self._indexes.append(columns) return True return False def create_index(self, columns, name=None, **kw): """Create an index to speed up queries on a table. If no ``name`` is given a random name is created. :: table.create_index(['name', 'country']) """ columns = [self._get_column_name(c) for c in ensure_list(columns)] with self.db.lock: if not self.exists: raise DatasetException("Table has not been created yet.") for column in columns: if not self.has_column(column): return if not self.has_index(columns): self._threading_warn() name = name or index_name(self.name, columns) columns = [self.table.c[c] for c in columns] # MySQL crashes out if you try to index very long text fields, # apparently. This defines (a somewhat random) prefix that # will be captured by the index, after which I assume the engine # conducts a more linear scan: mysql_length = {} for col in columns: if isinstance(col.type, MYSQL_LENGTH_TYPES): mysql_length[col.name] = 10 kw["mysql_length"] = mysql_length idx = Index(name, *columns, **kw) idx.create(self.db.executable) def find(self, *_clauses, **kwargs): """Perform a simple search on the table. Simply pass keyword arguments as ``filter``. :: results = table.find(country='France') results = table.find(country='France', year=1980) Using ``_limit``:: # just return the first 10 rows results = table.find(country='France', _limit=10) You can sort the results by single or multiple columns. Append a minus sign to the column name for descending order:: # sort results by a column 'year' results = table.find(country='France', order_by='year') # return all rows sorted by multiple columns (descending by year) results = table.find(order_by=['country', '-year']) You can also submit filters based on criteria other than equality, see :ref:`advanced_filters` for details. To run more complex queries with JOINs, or to perform GROUP BY-style aggregation, you can also use :py:meth:`db.query() ` to run raw SQL queries instead. """ if not self.exists: return iter([]) _limit = kwargs.pop("_limit", None) _offset = kwargs.pop("_offset", 0) order_by = kwargs.pop("order_by", None) _streamed = kwargs.pop("_streamed", False) _step = kwargs.pop("_step", QUERY_STEP) if _step is False or _step == 0: _step = None order_by = self._args_to_order_by(order_by) args = self._args_to_clause(kwargs, clauses=_clauses) query = self.table.select(whereclause=args, limit=_limit, offset=_offset) if len(order_by): query = query.order_by(*order_by) conn = self.db.executable if _streamed: conn = self.db.engine.connect() conn = conn.execution_options(stream_results=True) return ResultIter(conn.execute(query), row_type=self.db.row_type, step=_step) def find_one(self, *args, **kwargs): """Get a single result from the table. Works just like :py:meth:`find() ` but returns one result, or ``None``. :: row = table.find_one(country='United States') """ if not self.exists: return None kwargs["_limit"] = 1 kwargs["_step"] = None resiter = self.find(*args, **kwargs) try: for row in resiter: return row finally: resiter.close() def count(self, *_clauses, **kwargs): """Return the count of results for the given filter set.""" # NOTE: this does not have support for limit and offset since I can't # see how this is useful. Still, there might be compatibility issues # with people using these flags. Let's see how it goes. if not self.exists: return 0 args = self._args_to_clause(kwargs, clauses=_clauses) query = select([func.count()], whereclause=args) query = query.select_from(self.table) rp = self.db.executable.execute(query) return rp.fetchone()[0] def __len__(self): """Return the number of rows in the table.""" return self.count() def distinct(self, *args, **_filter): """Return all the unique (distinct) values for the given ``columns``. :: # returns only one row per year, ignoring the rest table.distinct('year') # works with multiple columns, too table.distinct('year', 'country') # you can also combine this with a filter table.distinct('year', country='China') """ if not self.exists: return iter([]) columns = [] clauses = [] for column in args: if isinstance(column, ClauseElement): clauses.append(column) else: if not self.has_column(column): raise DatasetException("No such column: %s" % column) columns.append(self.table.c[column]) clause = self._args_to_clause(_filter, clauses=clauses) if not len(columns): return iter([]) q = expression.select( columns, distinct=True, whereclause=clause, order_by=[c.asc() for c in columns], ) return self.db.query(q) # Legacy methods for running find queries. all = find def __iter__(self): """Return all rows of the table as simple dictionaries. Allows for iterating over all rows in the table without explicitly calling :py:meth:`find() `. :: for row in table: print(row) """ return self.find() def __repr__(self): """Get table representation.""" return "" % self.table.name dataset-1.6.2/dataset/types.py000066400000000000000000000025561445346461700163360ustar00rootroot00000000000000from datetime import datetime, date from sqlalchemy import Integer, UnicodeText, Float, BigInteger from sqlalchemy import String, Boolean, Date, DateTime, Unicode, JSON from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.types import TypeEngine, _Binary MYSQL_LENGTH_TYPES = (String, _Binary) class Types(object): """A holder class for easy access to SQLAlchemy type names.""" integer = Integer string = Unicode text = UnicodeText float = Float bigint = BigInteger boolean = Boolean date = Date datetime = DateTime def __init__(self, is_postgres=None): self.json = JSONB if is_postgres else JSON def guess(self, sample): """Given a single sample, guess the column type for the field. If the sample is an instance of an SQLAlchemy type, the type will be used instead. """ if isinstance(sample, TypeEngine): return sample if isinstance(sample, bool): return self.boolean elif isinstance(sample, int): return self.bigint elif isinstance(sample, float): return self.float elif isinstance(sample, datetime): return self.datetime elif isinstance(sample, date): return self.date elif isinstance(sample, dict): return self.json return self.text dataset-1.6.2/dataset/util.py000066400000000000000000000110461445346461700161410ustar00rootroot00000000000000from hashlib import sha1 from urllib.parse import urlparse, urlencode from collections import OrderedDict from sqlalchemy.exc import ResourceClosedError QUERY_STEP = 1000 row_type = OrderedDict try: # SQLAlchemy > 1.4.0, new row model. from sqlalchemy.engine import Row # noqa def convert_row(row_type, row): if row is None: return None return row_type(row._mapping.items()) except ImportError: # SQLAlchemy < 1.4.0, no _mapping. def convert_row(row_type, row): if row is None: return None return row_type(row.items()) class DatasetException(Exception): pass def iter_result_proxy(rp, step=None): """Iterate over the ResultProxy.""" while True: if step is None: chunk = rp.fetchall() else: chunk = rp.fetchmany(size=step) if not chunk: break for row in chunk: yield row def make_sqlite_url( path, cache=None, timeout=None, mode=None, check_same_thread=True, immutable=False, nolock=False, ): # NOTE: this PR # https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/1474/ # added support for URIs in SQLite # The full list of supported URIs is a combination of: # https://docs.python.org/3/library/sqlite3.html#sqlite3.connect # and # https://www.sqlite.org/uri.html params = {} if cache: assert cache in ("shared", "private") params["cache"] = cache if timeout: # Note: if timeout is None, it uses the default timeout params["timeout"] = timeout if mode: assert mode in ("ro", "rw", "rwc") params["mode"] = mode if nolock: params["nolock"] = 1 if immutable: params["immutable"] = 1 if not check_same_thread: params["check_same_thread"] = "false" if not params: return "sqlite:///" + path params["uri"] = "true" return "sqlite:///file:" + path + "?" + urlencode(params) class ResultIter(object): """SQLAlchemy ResultProxies are not iterable to get a list of dictionaries. This is to wrap them.""" def __init__(self, result_proxy, row_type=row_type, step=None): self.row_type = row_type self.result_proxy = result_proxy try: self.keys = list(result_proxy.keys()) self._iter = iter_result_proxy(result_proxy, step=step) except ResourceClosedError: self.keys = [] self._iter = iter([]) def __next__(self): try: return convert_row(self.row_type, next(self._iter)) except StopIteration: self.close() raise next = __next__ def __iter__(self): return self def close(self): self.result_proxy.close() def normalize_column_name(name): """Check if a string is a reasonable thing to use as a column name.""" if not isinstance(name, str): raise ValueError("%r is not a valid column name." % name) # limit to 63 characters name = name.strip()[:63] # column names can be 63 *bytes* max in postgresql if isinstance(name, str): while len(name.encode("utf-8")) >= 64: name = name[: len(name) - 1] if not len(name) or "." in name or "-" in name: raise ValueError("%r is not a valid column name." % name) return name def normalize_column_key(name): """Return a comparable column name.""" if name is None or not isinstance(name, str): return None return name.upper().strip().replace(" ", "") def normalize_table_name(name): """Check if the table name is obviously invalid.""" if not isinstance(name, str): raise ValueError("Invalid table name: %r" % name) name = name.strip()[:63] if not len(name): raise ValueError("Invalid table name: %r" % name) return name def safe_url(url): """Remove password from printed connection URLs.""" parsed = urlparse(url) if parsed.password is not None: pwd = ":%s@" % parsed.password url = url.replace(pwd, ":*****@") return url def index_name(table, columns): """Generate an artificial index name.""" sig = "||".join(columns) key = sha1(sig.encode("utf-8")).hexdigest()[:16] return "ix_%s_%s" % (table, key) def pad_chunk_columns(chunk, columns): """Given a set of items to be inserted, make sure they all have the same columns by padding columns with None if they are missing.""" for record in chunk: for column in columns: record.setdefault(column, None) return chunk dataset-1.6.2/docs/000077500000000000000000000000001445346461700141135ustar00rootroot00000000000000dataset-1.6.2/docs/.gitignore000066400000000000000000000000071445346461700161000ustar00rootroot00000000000000_build dataset-1.6.2/docs/Makefile000066400000000000000000000151561445346461700155630ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/dataset.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/dataset.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/dataset" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/dataset" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." dataset-1.6.2/docs/_static/000077500000000000000000000000001445346461700155415ustar00rootroot00000000000000dataset-1.6.2/docs/_static/dataset-logo-dark.png000066400000000000000000001303071445346461700215550ustar00rootroot00000000000000PNG  IHDRax IDATx^}w6@I"f@ūAQAQ1DQL`EEDQ F6Sug{AvywU[oI8:VOW@:6V Scql Scql)18mYCXf[Ӊv:W[*A誆}ӏ!eC;C=;ᔞz~}npedUjtYt5EӪբ 쯅d|R|ۇ-vлvFZBtܵ|XihBXOqBvC8B2>BuHBYoHH:8~hP$ߍ[dт?w}Y;tə]7%`uH 0 ǞG:Mwu"ZwSIBI7He_?M AWa { sqNt<;8>)]T dH b߮xmT7Ǟ?,{MؠB 0U*+Ŷ͛PRX ۇ`( CfGZZR&[ 3;3.j]X6qNb U|l83XQek: axJeOȿraOS]Ѽ5TP _j$"jlX;*^T4Mdb", b]t:rвi#{a?6N [aH֮レXA"HZ-7Ez QSh2Y5J ֯!+?^%zfNB qUٝPc;>zlں arr((WC>hApJLJX!)6(P)>,i!9>'|N9TddB#ukۋQYZ)+6*jjjؒԈ݊897r*+["RH] aks)(qżx # $hR ',kTz|Zi~/Jj=Ђ~$tʊV@Q`;aO=) PBh9'ۢ)eWddef^Ѽe3ddD;~>KQG.$!+3 Itv"!5oԅ_O̙794o5EBea|y j$T!PS`m+`6FYqD$NnːH 4at-9.ZC8`Ea$FPUk* faEjJ@Վ"[&)QDY.Eґ:s/=VW +OSB5gg{LA`N}>򱧠\"3Pt`ܹq2F2JV"IV5C1( +(0RY:Y #)Z8h*9M"M\$d,"!뢇آIEQ`a%@$ʊI4 4 II HKGS{|/ywYnᘒ1u/y8tMB#UWbPU.KPC>TCD[,`DV HV"EX D ĥd{:.Y$ * Jq )B!(W: y說"!!YYY\;f$"1o?w8$G$G]1V[Z%l]͘&YF]T>IBPa(K*tIlsbwꈃb8X,˜2YB b#iIxw"Aȝ"Bwx^X  I1^'nj>*![oE{Ieoъ`i>;J9 . sjO1e{b 鐝qQ8dSuMYg!7iAL.RĦb(1?g(͟0G\r3oioš+g}R~UvѠ#@2H:9ӟņ۹~@WڊBp͈GqB LB|F.Oe YfJ4SeNt.#c`Y0R$ E4YS-ֆ,aj1(h9mk)|M5q OzG ѣf.f[嬑F>Ycͺ PHPjKvUht(HA\Z.ϑ,( E Sd ?D pB0M`4m:n;0O<[EB ItH) .)q' o8#LTXm CPv'.XSp|>lT 1ŠW1O #)֐T\~jdܵ{ a2T9,Islmwfe΢E0v B>ᩜUd  G&`oi 4ո#T-Wh;$ʩ rC=\鍡Sb -ğ+FtvL1V-86bѪ]s׿"~O4-wu5:tSV_~M[vt` N!뤂0\uuMAEyPxʖCSt`){,^zz*aW":.ho Tc?, ?ƒ"-ZE0ܨ&*ݢf`~KR;eGY-*q@%qpHXS=c5N'S3a4j@~*@'\#rZrIǣY}ׄeͪa:+ t%N8DBav QPPj0tEb() s]_IFlHÕ7LEҰng4XOT>A$L㣅 lKJb4|zHB[R*\iM1\|?Zi2\)]l nDt?4ԣERL4PKf |[Ը}1پr-)la_\.'rXɯx7`SϏ|6[ e_D[gKa˯ks/A, ɠ!O5dv; jxJfp$%s)u]u|J8+G*Dd 5df!@jj:JKQP\0ߜ8!`"BMr,8߹EԴDX o,|&'s9ē49eL^$!Q"kAv r|{A٨ɞ[A*A;?-_b.̻=)2"kp =,Y`Zbwzj)?&(&%LHK223;EsqFw?yű? ϿLiuƓca/\\%%ߨ\ xu%7Jߡ?*wn.j2q$p> 6!݊=6u&=nvT9h8eaɕJGZr2"pN:Ә׊! k`O(WIp0SeZIk7Tu̾I L W rA*!ZH$I!CHMaW3rYrGzm:vݎe%/W% \p.Fyܹ κUk'55{=X`rѴM;k}Xr~pHP1 k t)Mbej}APlPzEâ/EbSBa~QURP8`Fn^^rVGkww"{uiWg>m; q!'HuYqHi -IJ%KrP:eEj埔US$8':ғ`[]FH.MΩ,Pӑ]TBWcꟙ+K1+ IuPaDt "l$"hNڡCh֞\W o6a` h]vo[e/?C`o6mЊgze՜&%'v#s?q&ujPeM*ՄkT2QЮ-[,XeY%tjǟٍ1hx p 8 nꌿ |(ܵk Oe {QQeY;lްPHFFAimjr8`sSJׇ}%Ha[иU+DdTVB c6~1z/' z Wrk(./#`c\tI#)![;PĄuMJQpA#2#d̼VINn4i9N> _னΝ;eVnRˑa559pg"%3d)B+,z [6oCD`u:9a@[/ t L(;e a4>"=U:MJӘZ.%&k3\!(L2!)3NϬ''5G>Ť%TZA~e/ߵ/lV{ @& 0A,ԟ)dSrP爁s:3E{ i$ T*`_Lu B̟&3nM$ hՈDF GP+Sk~vR"c5ˢXjq ݫVN#5MwĠ3kY9Mp7=f3qg)ݱ owb!aċpSP$ub8!eEtڹPUF~q T~3GJ jnbkZ tԢ#CD@8g IP$o#*D G㓡Z7 1fcYICDuM-J^tzײr8B8뛞`xl[~ĝq>%EuAE@Jȿ1d!p.q .)M'̊(CE$WӥyxiiU6b&5XvƄJ C xT#Axy> !%FJn/ G"ӲHuR\됬.]+ci`[ݢCt "h&\>'ky9?c r=> ^FBiIj<`(VKAQaNQjR tEaݻο `] ao?ۯS]ʪr4Fۺ=K\8 Ip:< ALρ?iѱ[7\u]Ն0sl#{cc|\Ї*_ mƁ%VxH4 /Wv9.ay]GSCsg(?**JLty[$_[/[PP7׿3w>kKZيbԺ=,V2 .T4)ϗ\y5QK`Ƙ۰uMТm]G4%Lu 'J:zᕘ lXEmK 9ӗE9YAZwASl|HnϮ̞Q9=H1<ڂ-Hn8xŋE !i.4Xt:$ zZvEz}~Re3"aP7pAaa̋h4S<$vzʂ^$6ISGEanZҩbbi]+}4f"voڄfƵ?<Y聊xwB" EݘK505_cgb Eߋ@1f5ڈ,IԳOY=EAO^GD# ײs((뛜=yV5ƳĎG3H%EF$$%AѤ][uHJ)H>˄ѣ:;**ˢX;k̟[pP@Jn<2%lټ /ȚPg_u<{7ljvpߡ .RUt +q;wdva; :jsrr;W IDATE|F:&t63ơdŞǻ)VL_I:CUb1Sogfm{¡azb2t*Lm4rxNOKƈ{GaehL!eE:ؤGP]^]fY*%xWʳa3D%@tgw<2Mz[w3yUwbGB6x(y*.E BU,L{p583?P%!)yE]HӐ|P-\ZMeElUՉ؂YxO;&q]/? }~ #؂kSO"YcfnX QTP`XEZZ 22ЯYhҦFr;ʣJ+![0aSxoxwˆ /jbiE;uFK+.Z-ۇnZs`G7kwߧqP7e3q=H5sDYm{BVV&"#dmf vgU i"ob-B^}0nhnu A_hI; t0/y%eDڶA*{lwKklX Sp߄xrܝذ+_@[(cGHy);N:QY5#){&>kϏ˗{:+RoUc-Ls/"1I8AWJKxNjgڶEv}QТ$Eĸnp񘐒"c0|pa[ 9T/0tn3h.a#q;bjCV4&2 .πubm&Chxs3c[y`pE E x)ђg3??%'\{vl2tw-\U |h,)PcD+++q3Wf!zш$TM&% sg6hE;!yk3' bP`b`b6#b`XJF/c#pbܨ(N:̠b9YB l"|jGu|\LHLEԧTvưA&$_D,.{r7tX1]1 B~z;Yi0k\dЎZ$;ZxZ4 fLy~&) $(W]v>N{>xE'u֍6AtgxGYB7{gXqн/m8$!byLExAHlMd$jn ?|'OzX)QOQw?|U CVe_OVqs={<4˃WO/ &H ( $u7]F-C FpψQ(eB agcZ5sEdV(/ :uDNn.᮪FYI1֯߈r7ۘ=B$mOdv,~lB_OC1g0gEƲDD 5à!!=3 &=7l3!y+p ڣW θxd!''g&&'!Isl q_E̞N.I9$"1aJ#,$8u?Ĕ,&./Ƕ-PRR=@VfpxC!_|+WbtgK8s(` Ți** QBd >4?䅧9j?VwY cSn\VrCG$qp3pT oPI)h!#sw֢ )sSVt;'z鋴<>L "y)d}ۻ;o,|;eF w$]0Bܟ^Kn_zA(W~wzmAHUw'.IU{e>,t%CVtlpH"[Rn +Sִd=6h AO^9"LI AzB̺#d9HMLS{ V芆\t^lX ✺B޶6mF.£?4 q.\qy)vbLɮbTt<+!r IxqXj54B kIO `wB TB zǃlUBKpFୗ_ŏ%˰,snu0c4bc 2oqǵbzt5Topꖂ߾_+~MA cwmDV}#,챽Ip9E CߘM[סо<zl%eNHc$!#=Eyżs"ڃd\Z|"b| mƸ YKLx5 ;dŤ_EѥB~s<!F/#cƢϊ˽/T܌rb.fE ǵà;FqCkߏ?X`puc٭"耻/}yoJޝDB+#A9̚1`61 lbD9p-)Huu5gx8^0'u3eIz m" o W*y8Ըϱ}4p\t "TxGyqov41* fMIbOy7bmMD@+0X<%|7CE0{B ׻AQb=B=nlANUp{H/h*+rpM07lx|py^ f\BxjW^}8S?xkR( J**4 [Hnnz!T]>zH7(͟4gިKwO$ME*;!~dy([S&. h&(Ȫca7K\_?ђb&1C`Tc✼+:#) :#˄P5щwYhݕ2jkҢ=" ~Y4T׺tӪYk;a!n *:ڮ4J!,nQ|+[&=rLA)?]&Ho @@b 3[E)`E1".`KZ$f+#^`%س]J )^_ς2JρBܷ$B\T B Abg5+">/=\vs7Qxnlxߥ#nv ˾0YTv{!F14l03>BĝZ5-n;nVS؅+-i{ǽl EӒh(% "չJunсMJ}eeQ·;9Bn fnd C}jykT401 XmŜ5hߙA6g 9"r30q<⒓1_#THHǙgCC׃Fދ$:K!kшZ[b*qaI#>3Mŕ?%ys4ק-^F;`ʓdI`SX `؉هwL{?%w']]z^"  E?v߿cGx o51}@ꔉ F#T{Ph5[+!z\P Drs1 q9ä.eE2SJH.IL%[pygeBu1vx+ax r\'|r5*iEUweڌk6W=1$hPO§UKbք  2yz{2JvafcHhII\.3$w1+$/--ZByM~߸24;~3u\Sa ebFB>l 0Fv6-G]x?o];GxPzi돐5?"Az{vhӺ93ц޽b=[UxQ7j4F#-xHkp|j`/)_Ѽ3-J'm{[ 7I7_$h4Ha 2KЩSg|VW*f؁ λn?ͳ }_7.(<BHAsD^Z gs. Ͼ/?_ Vh6WTN9L$';BY@$y?\yP":uC(*ޢcJD h;x[WrQ S[z6N| %"ZU~;6۷D[Sl/<΁9uXQȢx<ğeL=c+x̞Q]Ŵ"}z8Wo!W D(pLVCA }V\kjha6pŁwߎܼ<+Ԟx;/(v4&s9eaw&CV?}t4oNG8_~{ m ncӼUs#3+V+ 2z8':c.|y` TN%L/98ZI x{ݑ{&;h'J0 x-%tYevx͙[]˃AiV!µmx9X8o JHa`<3zѬHJ$vit?^1~ abV).([F#bTS!:w'Gdd0w?5k_/êjUp]#1;P]QD|FM0$W&8<{iE yѪM'<8 <5i*kRstZwD!Iji1o!7G ź,csIB̤Xwlq`v=Yp%Ogcg`KWcxUNenEDT2Ddm*CF7 vJ!.!̇6@ЇG&N/5, %V[o^1C@;ncn{2xj*1čCJ” OqJ(Y^Go ]fa7˿.bޯn:= 2gWz\V̞2[@]xYT$hJFF*SO3}BpV~7bmqCPZ&K5/G۲Q&8o- a pX@vg555XjHIDyؚI2L &}*/ID F`ݘ3y$H͘h~Y]%Bb+X5, |oq(B~$!;_;bBQ(Ԡ*YɎ-™n.LE!!\F0*sm젉$u Z9|6-a+f-E+!%#H5b %|8P 5vSgMÂ^%.XeN<5a|s|HL˃ 1ux'w QY]> zɄlۻ1c*֭^Ƒ|^/#-OA OC%#(0no7-ѢSt@jF1HP PʢѶCG>\($$_U% Ay>6Cm bx +:̙Ņ9.j;QBkc"7.qUƨCQR\EyH9;0'oFZNKj*pFV<̚ 3zK`!xl؍ؼz-#DZv1=Nrba]xk "fQ>bv fLq[ыr#aht`S{M(ӌˆr݃‚(.GD"Y ,z%WVqRҸUUn11JEd3!yw/Z<zp&#*c̉>%޾9M-߇&߅ ǒ/W7\5N D b̀ (s(rr1VA~ =)`,{>\v Dɒ͚MKJ-@8N `γ CkNJ GE),wp٩G1q}^T !) )63~<\&Nipn<6k:ikI'ոc¨+,'Ҵe 7@: Y1\$EE%\6dJ"8HXt.$D0R"r8`oBp4 .h ;GrZq.2C"xkH2HUV00`[149>L<7i&ʼ6fw P]YllfcpW"9=C7EGj[}R ?s˅S=i+a6`(ZyU5u Ld>FleĦr/KiaSQHqTZ34lI®hܬ)]vg c6k c!h"]$ğ8yX]}h<Lj r߰vͯ}8lւ-]W1zeػz) +nQιJ'1jtOcW&"hyH嚻>1p||Č](fyGaB9pqMd%)a?*+( W42 33^teP*ůWa`:+ jȩc83s~=WlǷ^{bcñQ\AbJ z<]z٣ 9z' ջEy}:]&_]f̈́Dp{,s!X$K[=T ()m͂a$ۃ-`M2f+!L(9i9p՗ -pUE}:utI#e .(ek8sG\H *šn3pRGDh"LzgTӆR]ɔ8hu|g翀3&;y+Bw~^-ȃ<9\;yFKV !#d.L XXla6hY&N_(,) )`^'r=Zs΁͕ħTr(EiAA/R\ABj&M;@ř rRİ1cPݦփt72WLYW>A#`"XT2%Iŀ!IKXR&Adk@[2𦒘1K챜B&`"4etV*٭6`X]'t0e(&༵>8]hA#G棤Fd^ 5۽* k2ZQD-x.ǵCrJtFuF.n?Ջt(Fr"Ҹ%au$gO0[aMJ>·, bAާ«  ״$ QX(I}䊂 ,"VhUajw` ꪊ`Xrs1@JH@&9m]O9:ïqcN83E+b)_W4k@jnS&ٻ $ؒJOAl?Jw`O?^]A/}]ϠO&K@dh/İ;%΁/Y}u)j+ {;\qvo܄gB5}.ա(ǡ.|b\2['#,\3xgL&=ЁX1ـ%h44͛u[U6Z¢(H͆SYϾi^(Reo XBL-H(`w9`X E}(]/P~cO@oڹ;ٳQQԟF9Vhlx< K>EeV“J|&t@"ӡuHA#,՟KN5󗩆Ě$̂61mT1ꔄ$`ޒ]qWOeڡzS|Ioխ7!+_uLsA4bn}O{9)ä<6@I}tUg'!5w-JC)Zw"JRR!\=yι  s.#ϙy[b`.+٨DId' DT'-=½Bc[lERESnѰM{Gd% 6E6CË'H;6j! ԓN 9ĠQPtbXF||7!LpɀX8`1^l?W)H%KzIKͱf$zFW ŢnC#lOě~/As9_iY6z  e)N1Ֆ>dElR+&vir_ .%sR;H wDx'͖fgYZWV$beslf&0h`lhu:G'(h\&C!<"qV#@RhW)һn5s)G ~ E IcL ۋ-j"NZt4Ν±hpBR*Q/`%`6b1avݔd+FclعX*INS`M2 T4ݳbʹb4%5_=%Z,Ș@Cn{YdC`3fâJԂ\Y3E.X8}:*CmNѶrtԬnc`TK>s'>~/Uf= B9#С(?/<(b8QsJ}.Y/0c,"4-!]n@r,xPQD(Ljh*2z[<;]6ebcBpȜ^aVp/a z +󕱒OP"S[LFYaYHIf΀]@uĪ3գX0y**ը}&K+^(1JC>5Z<珟wP˜P$SLo>Ft)PI ".*"Nrx UYŐP2T4{9G>8d"q{ϡsp}lb; j rmZ"Obشp!cEe,ZStAjuգwt@ּСRtwZ88]j =Mwµ"ZO~<\ | Ȃ6ġ}U5T[=Q˯H0.Sny+/FF&\>RxGGaߒj QGr% vضrhԆGFruvqF s`6Z0 +}V،:~$%rö S cQc#čDd_1*Ywc6DAF=vѴ*xdrmB J J!R =JA4LG:GΜKFwƺcp,wF) =d ^i.Kd 4KẐ>wh?%WOzڷBdtX+"<,3_䦕.ꉟF]`"MLѨ#ONq%ȢTNi]Q>^‚)EIe Ct8`$&M)4 1M"%ىI)A5 jxH$x鿒IlopurfdB\QJx/)Lo}Igݷ ;.=ޢ5K{M2gY[\Rz7CDҋѣ7oc`&cFpW='yF 93UK-©"=/A x8oS([aa$ҥJb (\0BBp9>}P&^)J uqCŸ$42 뼀g2b4Ao#hӡ@K{g\x3ꫪ(Y&?M e"k7cKFaUa,GŹ'%ش)ISiҦnϢ@F2XE^&pUEՆMl,vR_@DD"W,5ue![hS 6Ӹ|*Byx.ž(*!clxp"X MYRg\yey1J:wy ܃Zhӽ'ܺp JUε0@a"̻Fڑr;9ey!tn~gu#:4>*ԨMuIX^kz,?:7L$'޽ o_"6)3D܅p@gKjo+oUh7XDI_ NiMɕ~=z.LusJZ)7?..M}aShܼnNc՚wZPfAHMM_ĖL ʀ#!W"8w mV%a4o6 &8ⵙC5 'DvZNڱE#)HN5IPɓU򺙡١K(W+0ywAao^ǒūZW Sfwʜ*q :u*Mڸ IDAT]R8 ؾcwb#aF=䂹^9*^ fU\]QM'Ar vvptq!ƀ _<%g~&J1,󚅻O|8LNP͋8RFHj)-Ĕh/^rCI 4Jo.itot ֨׮`R"F*j墢 Nv eDBa2sV3 ?Z4Z$=[a }d=Q|e-&S`)2NV9dDK#WMCC%XhNaJizc~HbOnSeAhN#獍nL|Y=ТOF$ e/fwPwdS?8@D!.%V4쁢bX" (Xk"5CkR!s&G4!mVt::G8) Ō)5F PT3dblSrd)*P"#&)gUۻB : p0,p.KkfwWl\ ׎ h=ERTZ^{ԾYỼ41x[pzGiuvРǠ},{GժR Fx` ?rkX|5qdGE۶j4b{ f8BZ,Ȕf.7J,jc9cerԐ,4+|ݺ5ngSUF=UT8t9 9k^dsv6lD.G"qsͶŌ"X`GsΰU>V 2災;0bo.&ZQa-02j qa>0ĂRf/^pt!i sEx\pe|v-3{0M1zfr.;,!*&b;|h`$v,⊩&cG^ cvи,:1;"шY{e;֊=I?"f!>Z8fkl|n?u5:r<%V؂2ؔ72EŒY!?,r76kR8xpaQ5L f D >sٵJWCCc硲Z[k)xG:ݔ87xVNѐEEWa YlXHӰs2y͞ X8c=+FVs[Ղ}͞ n1kNs9LKM;{TvCVnDy9]ѥ_>Wn=)f6@/ap>Pk\|(?e3äҡw^й:cPk7Q1Ш(CACkfLk"mj8 6Ѹ~m|Ӧ5v߀ e}`=⑱P|Y~Ok|D>><<1_5-P2\2v\ǝ2 v.Ȑ9}<?#f‡G",(f;@CEan-5cppvdD.T>}R^9^\!krJ dϘząpUhu#$!aЯ'n_g.E h)^SkjW) ub%3:}˳:nF%f. 3ԦxtYA -a- r eʬ Kdts}]7!dA>5j{  )a32fpD>o<} ^fE l/,O}Ű*AY)2{9<܀ym( %ɐ{ hT) D?KxH'̎sbbI`6bЧ7!l a~oMG%/B兲%-\D8 ˷8"gq`HlCN4P糋hIQ"n&wSS WDwPbLX8o:-bLK#D;-T)[}{q}1O,F ;N Џ˖qIb!Wm-0%S+aRjո6og94UZ& ~~L#hIҶ: j"з/a^K>EGӛG%QxySkI IcpΘSfVW:O" aa2֎ *ʓ 'cLJ9wW)=TMaS{Hb"t({Sv[+) ZmfhԱ_x-(`f"., kVg(SV[n {1(,ئ5gBVxԄqiQۺ傜2}xLE+WwEFkAxO kF(Umم+,GpLpQJQˤ>;;G8r5@̒::capϕfx+ LBgAo!"X`2b}кY}n&;:chd޶"񡸇, Z=zwE/`ɼŘb1J@,%juqΫQc6e?}i3)raG˻1}̙԰0X@;<'jh]IP93Ϯ{ P7kĒKPv\\ ѓγNwjk.Ȑ9+ z\>hN`?x#nވ L(,JWT"իѢ{n ôs1bdϛFήL1QqVׇη6R􂓋=^f9]Jڀ/BY5MEC(^&M a!8{4?z 88hMkU. _59`Ј~øC0el\DEcFѮo?&v! [47.:..UmӒ{v&~@ȕ`Zv!153oё1'\$o. {7o#K<(Y2ΘMA̕ׯ_G|<Dף-bIiU ,N*L_g-޼@ l"@='gA9:`̹x̛=lEgqsϳ(b҇I "'VF0Q<Mj5*{'s2(]"f#GGB0Pi fdԀQ]Wy`$3^'ށIŌ2v\S,W . ZZj\TyM5e3G$qږ^*G]*VH] ) Khf`\C~c8povݨ}5-Att4I&s=X ,=X-oFi 1g_g"0 RFJXdrx`Ĩ#LIC䚊- ]ˤYOdeU$:܋5J- V|UQw@8bx_-V zqC3Ъŷ,Uj W,:ן1qju}X0t fKs>x}y fm?HX5( |\HK,G&9aBjYM?O~:Q0,pHXrE $7 &6>^r Z9˖_q}lo6e":4kWn@@#ٌD6fz-2MGΥM/'+(J!KPbLz+FDQ0|H_+Q zA1zbz) ?7.]XNd,F5:|!>&=ڍ1mi.i~/s_6aG-xǪJ8SaKFIEkDT6ԣ;8[1T?\eUk0-GPN 4(}[r!X6c*p2$_l֐ŋoFDdĹ |&L ܲeʼn{{T7Q I`ZUa2dGb˔!c4ʖn@(\/,(5p`w,]1a7b4Ty*"OrxAIN1]fΟ b߇f[wW= . |۷lv @ a~46nށV-vصhvn\frfJiWi?Gw-S)ˤSuq|J{ǿsjRsZmK@Ӗ)u¼_g@#cر5GCg^V(FlI6 .x.,倖Dɜrsy:qO;>j+ dˡ.yL;8M;qs5֨ 7b8{ aL%2 $1ѵ VSGBpx-ƳG0yx44Pj^T^f]`"\;#ĭ9K'LiQYS(͚5"aPL~'w%$uhߺ7>+W3Z6Xh;;]y,լ(ǩRm E!W&Wq;c>1{.^.e*yrgEULnB}I%+V&, рgHDqRR2m>JqBEZ`-K G [q)kl$ bY`11h\8~.LP('{"[BI8c?!QނCF`󍨪 IDAT;Oَv_pHog⁶ݺN܄#1!YٳcࡰeiROi-wJ*t:dϞN(3 uĂxH;E7jPJy|W"%x ^šឃK$>y_: 3l**?O|Z|B0K<"Dxp,_ -lFm=/ș9g+WCNvPA2\j-u5)3`ϝM;kV?Pt?*Dt\) C[W@XK^AL%1\z׬K$(g|]77%999ݝc#Z5K@KolwH-4jDöX1:T8ɋK˔*_ж[.?o0Zu&@>Qqp˒a>/;ͱv`i&ivyOf<3\N0UPT&r-kSNa,ȠSa k fۻ3 / =;\]y4G&/)qH* =~\`n2"B ߧ7aµ"!!p&-C]0GoF5Ԉd *RwC1 ԛAd4yn=)k'!;FcǷ10k[/? H4 "; C毂ޢŸ6 Im]}{w?2|pߦux/*t(DKKb,Z/2V-Z7eWbכ-đ6`*QD W%5LbMAL 񯕅]fݱ▐HFfMABqdn\v117@qD\T_܂WXX9w6 ՈZPH*W&,2KkzKpt *xt*XF͔Y VKpQ=LP| RZQ`ggWWW:5gF"0 *S<S\CNȚ;73ќڵfL~MΖ?!f+P\ i,>_*^) /¤>]MN),ۿQ4~M*"ðxrv$v|!CC~%i%&* p/ N44] ??DFCC- ,[L.2L 3Pȭ =Fh3ZEth׫/vmZW>ႤZk &t# 8 4r% &GlD(v>JhDLTcJ⯒ZYd̘kwOd('j8T[JWWߧ1ЮsdSr}qP+yS3{[!~*4i]'CMN t7m~:yBj:$ާ&xgцM.Qj!wXj 7.$$A!ab4ۃ, ;=Ιf7wP&nD+^_6kcv"d6p6M)'.5WzvS6I$Ʈ&߬ nZ sV2s;lH/{;11N(JfL1:v[WE 1vHU8p MC/`ҥhۻ;z,]'c6W/ņ;?kRSFB/BŚXa3rH$HHIzZ*T(_#F4aIqaعzvoZmoAm#*%d .vWbL X{7GbajdI2cР o2H4:djA2nY3cv^)LU?F KFF .;.C?q}tO鳗@h\IH\8gN޹ve>&v⡣t-rx>N$[V(7@' MpҬJI@kơma<}r Bԭ.?@Nو[/đ9Zgw.r..ض]U,Cӂ&MpA,O۩DBѠV+xdldudvtXوCm@Gdѩh׮Rkc٨T רO5O 5}-j5j_oƮMo^SY*{DX g1&eF]1k6nbRʒ؎qyܾvS/9:9Nff3?:  SFa=L<{O *ASGHiU94> O3rʆ:ctX,n,ߤG_ ī5ʒF33\5nk/^x3Ed%J;r[EV\FB^l5RVpϟL@dfzKHe`dt3bǁL!hs̠DEA-WZ nLТs7lX*{,%7I[Hɶ8e$B;VnܟAnY(us;@.3|>`=TBلo~v?{̘?cCl=lHD)Ua0xKv\ge` ©qC΄AB!C8~N r {gL=Zthsc-Mt"P9`$YKbak*ysdAM%^=޵bdQՑwn don<\…%1$e.IF\I˴6 \ %w sCQbU8dJ&ӨUl ޻+ 04 qT? "ط •wiH2pe\[LFˌ,R18ԧ?ݹ}s t*3:6!(BLucpf`Ѣpu'7EZpEt6 R'qʔBɲX )VRՓسq%LxP%PbbbtOBAJ(!9::Z{SrTwY )_C+ݢ"kdQ]v;G'W+Z ET@&•n5,F##1j7224aJ ,1 fV.DH6s]~߱7eLp Tj5{B*5.\ }SNSP(zfshd76n,X,QfI7WgtDO"BIƋ]=L(j ?u.Yq!96oήLD"g֬EMJ_9ݭL "/~ReC[K&y){.kE֎&m{&XYZdU 56Éiƍki"cP[*˻ ea Dq+jٲeCY0\ FL CnY%?~ .^LTlic<`v(Y(J C·|ys8u kjZX)vw.Y`势i1p*Mֈ]-+B'!aĺ+&>ז#x͝;Sz?In ,E]4K&P'*bwfȯ7S@]'^jGGޅ#}^ K_FK*>66^V1iC6Q n,C>*Spe7~:ڇj<)SMIRl2jLE rjW-!;bh!w)BA)K $ޭ*g絍 lk ʦA|l +>ZyHI+VEDt>l<̉S|~G>+Cx<{メ)a'=ׇ#(iүMYIL>}=f1c>/Ogt'Vೂ |VO]~~`>+Ogt'Vೂ | RH1gDpþzVt}BA#(,x0#A 耥(O> .I%^4׆3|b>G!\vfqѬBCQT>~#4׆'SVP׵|)WBA63|g>:Zԥi5^1ݚbƺ w9='&G'\k6䳂+i֤WtlowAS>5L+J:N^**Pߋ(Zr?țz®sFڬ: RNk|[iq$iw;ңGC`3ȒM]A\PGJǷx-}w P Vn8\΀3R?țzۂfCA &5oq h峋> l]p8rAwSZU2kӇ|&.?O_n?)pz{R 4 g|Șfܻ?\p\cxto5_XhD0Xpϭ2(*[T@~_ٳgGvn>-k\P,J* ;F}$}+EXUV*LHF<ׯ…sg[[\1{0 {Y^ AF"űGiN^&.X#Yܜ}6XET`c~R֭PB9du k1a,< |m],-M_*ca8k+~?C_;oäcg[3O\{KՋyt46YSW~)u:uSͲ߃k<-UΑ&b˱2KݺuF.Ppw{Nc,zoU;]>vNjUT_㟍e_R<-SA#nZS6w6UvJ?4aiAzwvX <Z oۿ_'yG&*V _EإҺ}K޹n#ϼ}GPrUt#&(ҍQaDkөKO^gAˎѨN>Gl͟9.ص ^[\# wpeɋJ.ۢR Q/PLe\zhazm;70%+QahwԤxfsy4|Bٵ̞j9=ĄA5q;z ̈́`H|lT̜ftDkqEMAUo"]w0a)u2g"eu;v^B֗۩aEl>g&<-;MLi 7ًx[7mviLm(6)^t+M sкdf)${ؿb,n:tVfRIDAT(' 4[vM;Vllk=~ג hDFG6qN"WZDZn7u~T%c6pwN,+Ȼo[ϼeIWB&1ocwictqhsZǶ;Eڱduݏm~='&̑_T(]7^$׭zI\qի63c4Ё^"/neҟK'YO=y@{L\5U(YeKRo+agD^_o (+L4i1)]'osPeF%N+vz6ΙçzjK=Eq7`h;yoTB|5pE8s_^,2sCx8oC:kUAr䫕ڵ_.7us NGZMS}khq[`ښNjkuܱ]yNѯהy񃅂慦tuH7Nn>PjN~^ԽHZ1 z=nաdKq7]K%ߢ4KЗzOX.-htX 5!YA^jI `H5y69+ =ϐV%dONA6I& RL]Ъ%f?whȏ|_ tl;\LƓtGۺ6։(Uw:W~ZӪ)hsBk]:{g tt&^7s +7 +9db RzpDijT]pe~/ yT3#9f\]]`6A3CDaQN31v/B?+Hj~Ҽ=G7R.3`.'KXI/e \,K[MIb_a_侀xzg*XA"2;;.k 4{(MfA"H(]ćED'Ċ;؈ ł@(=~ 2k6 5" X,*&y:YR#ڴf<Կ7:趓AC>rs'(n&.)u(rv!...Un=-Ec)>r/KIpi/j$Ad>ݽ[L~Bbs?6ۖ8!g"^t~=?,iۉv(짏pstdg ql̢b"vK7Y)`hP[=.S{chXL } |$A Gfݴf>yF1S!z(:Z*@2a,XH~NΦmح gcy]ap6.48y)urQh|^v=NXUDxAg:q 4'.`a!ȩ?U,Rh'l8/6rE塴ߋ{~Z ?< -='zS)o FYxo:J}Fro׈=)8Act-I/'oTS/K'sQJ9pKPœorTªAZwԔhZxq? i?ЖAVGߗ [~3\lLA| eS/#Hۼ\)56vv}r5 a<GJH]uZ:!ȀœI(OwekhLʢ%q:O@ϊ$7hPm͑RFQqio@[UsP>ͅ$^9nꏍO#[QP~,Vjbɇ F6[_Vut `>hTkhwnH0x*Lˡ^t$ )zZ["!fqu&-|z ˫/;O$Wnhf"fyOX׊^FOMMf#?rw!ɅSrTexEJ"fL2&e_ȆÙA:ں86%ߤaUu|Z{s[fIG6lkL Ya^ƪУj8h ,ۃFUDboZV5-9G*S!>fܳ&mG[JME#.*/2cAj%Z8KVQ qƁ&{)`h %p Y9oOKàu dsXIG~^$ULįX6#dOJYрI1KWv1.Xq7,r`4Vf)S vy ޼|b**j&f'ʗw N244P)7fpU&wBDCt:__^;bӨ-TOڋѬdJ"%\EV4hi>yq@7*l ŕXdE}5u|Vua =m`ma&e$o،W jnE_e9c%ߓHMKEzz&HE͛A6aai VZZ]8X0Vi2'#Q>CUUjhڢLѢۙ p.C KWf$ڍHIɄ&$ď9Fc[P J)U2оS`oWM# %PW/wǗ \?'EM0eJ,¡sw>@˾WV^8 ѠeVnE\ ZRXX^E18y>OCJ ۲>nyfN uS>XSm Z]d6rXNcٱ4ˮJZZpy:["I1CMMyyysg±d:+h_db3^sa9l(DzRuQ'B07DBeܹݹc@9*0'"J=JF)ts3\>I;Yޓao-BZ29^=1ߍ3p62{f*Бwc­R;hOe Q.hQǏ!^^ YZifޙ6k5UUU9Bro###*M1JKCadlB͛5C}UH$E,ɏQ*o@e¶O' b\5ijX#Q$e^FЈ5m-- 6Ȑ ay +-Ftڍ7lUUCP 5Ym6@@alT@axƀropjG f*@pjG f*@pjG f*@pjG f*@pjG f*@pjG f*@pjG f*RvZJdIENDB`dataset-1.6.2/docs/_static/dataset-logo-light.png000066400000000000000000001303341445346461700217430ustar00rootroot00000000000000PNG  IHDRax IDATx^}aNؼl$J F$,A$PPQQPD0 *+#"Ff'vT st:zK¡ס8ҡ9VWC+?VC+pHAk+pȂúmݲ[$''p@Rdd4As?MCu/Oe/~kYi `hP ,Cңu@RThΟV+˃mpY}pn~֭6~Y maHh0t:8qԄ1_7Ύ i?]~ @  th|$I0L$d 0 0$y,d٭ۣ~-q05$CD7 @D`HEcOİ;9?jԡ0acןB5xYKDA E1eʿolÍ3/ˏa`Hb_DP8]Ð ZuUCGᘓO9aeW/gc<ݐ0BeJ 9=pUV Q]Qjغ%E1BDz!3uK ]KMf U "PVʊ ]M@TG݃^~+1^zb"Jn"50WC:]G"vtf,ˀBСBeAVK@ 0w`'$ ^:KD/9\8ڭ ^7t- QǐUl޺˿Y]!dD%R* TvIs+?Be[1J6Bn6Ἃ/Dv`#'rtCv!]@8M4t-C C"e$Aº@(:PYZ\HL%EJF:a)HLLdKfWPTPXXwlH!% %7e=R5WMaYk T~)Hٵ]<)'x!;\b''A ddA#! TWPu9rOES+(.)7_/PYRpr ~8."de"CqEM]=K򜹨.)~,K :G,<R95dW;~FKIHŀk]0"a[b2p$$Qac GDX@HUٵz4TؠAV&+,p`*HDbV\! ? "JVbst/<$@{E\Cc=CEfJRIñ=C5a$™' Ɉl.A),ᐒB@3%EakQk#$ JBV(*b.-ah4 PQ@B-([٠C6@Q]]2,,s-3_:?=Af>etH&NJ"9I7pA&PUH(:ՠxFHV2V~EԀnPE@4s|"Kɀ"Ɉ BP\BP 5Nn)(Eqq1BW< 8#~ٸkt{c3ƛ3"Z 7 &]39V0>BPa* $C"# zDղYB #eI"Aȝ!"j,V ߅{Rz) j2:P1Oc}|@-3xǸCaԙ.-J##րnO1ePM%5eu0"AqY8dS MUg!7eA,.RĖbh>g(0֋`}= JP^Yk"$aqԉ'tOpP-ڻs_0O;e$ 7;xg\$怫mf|[G#hg @5J }-cȢ6Kb*usY++2٨bPm*!CXjA.6f +PS. iUDFF8FANl/ěs.c(s\t;A)[8gG9k$.Лнkgh$(Z m rhH|zn9@{DVgU@T<tGxG` b%7HBDթv!b\2OKzLirPAA 621$N9,T U5u(),wޅbK(2^~ATłM.pB0M`_;rul;0No*9DB ItH) .%z$q' o58#LRDCPBu~D5S.DnlT 1ŠWO(#)lQVZ ]oÈ}~<&[;:YrbSRpTCEY9u~h"lI( rBJIs%™ :I߯rx<M&4εqmeEŨ{`p88??t@^fN5{)|@atXs3K.Lۈ:3"QS3Q֐[$\*zujPSGm `8֬c+C`ve3@n^3 :N;N'.v; Tv!!s]qhb‡Pt0 ƱQ0`蒊z8]TVM ߧ4مZ=c8CThqTң%e!10S!RP.vB4?Gt4ҭzliV|h1T= 7l/) ]uk MOE@mAI0PuR  u^/RR&AD;g*PVVb' 뢿ř?>}tP]~Ǵ3T@zO rI<^x: Fx AE:ٝp$Q Kct"CƦp&pd$5"׉%?+|;Z<u/%@eoQWWnݺ#//HLiA4.RJb/7n;vB8FiU=xn&05CFgGL\sƱFLdje {m$9NJ)VNs* ҚwA\.WW_Z(x\#OZd`fHpw`WǥK(b\')OB CaX גQVpN𵈸0ohV V8|s MRIO4Źo5_ 7d?;nZ(F!MN`= ) ŝoF[~_ *K/!sP賊:'()-G] 2dfe 7+6 9;gp,B8.@AXr,8߹#(/-DEy +HkS/:7~I-i̻v%Tg2vd7  Ýтkھ U*"F/)=J%f9EHB!dMAQT]mЍ("jk.RVoڅOzj Wv?we{ &(=h2&6+zn s*G깘'2PqZX חO| < oڊbA|8ة[ʲr؉7"cWz6poB  FX ۇ -MQA Ԥtx.,&# y̢?DapazG&ɈQSIar:ƩPcjEGZJ 3pQ+Vۯ2bV5X3^Aާ{c yWD0VVcִQs+Dr,n_$%9UPkVB5ygwk ⷌ9I'_r.=,D$b!y *gzD۬U$DچlEV|3o6,e M qpPW@';/X[Xkkee%kk/T.IK..JTL4`É$W5UlI⊙j j #=zr},KgnTWBTף o{I@JN{MB&3ߟ6{t{.E#G C.UZ(d2NYXreJP^Ua/?_r浢cH%ZyrLOt;2qCeB;?)%q̙ٳX4OR9, Uw'&\vm=E4>b`ue(ڹAIY͐ӂcIנEQ_[dEbFN6v1HMME]] ~^l)KNDۅkd֦` 9u$yh0fx;7m&K3%DMeJStʹi^NA!P|iiګoWƵ3`Cum n܂vA74)&m>:g(#:ZUGKcZp?;TtQG֭[}vPp# j.ʨbůO>CeI1CR]NVoJ.;*0 -HJ: Lvw%ӟҝ;yJ6/i 猪2qH71QywFﯢPZ!f 0ZLFi]j E UAEBŢ~˕o-` BWlV 9 I t2 6(li:D##aS/TJc3~0oIp.7 +xh$ 7€6zYg/7pѻ#>RZ}f驈h;j_n:&z*o^ohPX!j-̄"S#SZel`-ڢ4u0Qbº6)"bޝy"{봈zAp*jwL٩;{IeڨdC߽ ?VmpEf9"7& \Z`SLA;5N.u(:QR{ZYPjADX*74丹Avol) aL8E& @#ЈNI 85(.f(0L13={ /=wÆ M϶_Ԍ'n$Sn0- {lEJ0QE\}6_]q K~+ϙp~<(l*fmðۏWV}oI}}9Qr_z3Dz9YYHy!G8uxh]fS/f0eeIϠ]<!ǂۆXJN40{o[(El^bVș/*TjnY 2#5?z+{F"ks@֠jZp^,bnu[ぷ=s}3 Ѻ1yxIčwoԲ/¿/4|F!CF!ʽ.9993Oձc{%F=8B?HQcGDž~=&7mܴ$,S9,b 6aޤѲcG zaUi\؆hGw“!!..\7YEdnF>kvF_KWs|߅r,&~[c z)jXxtPe"б-ZvEF}mTn 62Vx;sԐ ݉jϛ㉙S9( yo/ X‹2f7(#v^1\%֫!,V4w0v;Y0B$bcǃ IDATE]i=lrgЉb2bݴF\՛VBt?/<4QϾ(Өo7~¡H6 s $*Lm4%z,#.+´G'^'b~24["Fu g}HJ2̓, tRܰ"H݉PR 6/?/f7 7ZxZvC x{Sm Mq@",Lw8LhU(Bj]0%Sb.g˨N|7]{1ÕWQϟ ߨop'^^CofCvn.6(.-Ebdžu +( ̮hoŸQwט@aANHl 45z\xx㐉a]] ]Ec(ްySptN;lQ1Mnc4^B^*03vF֣6&芎h Ic迂2$:JIY Úd)n?*5濲A" eg!&#o=) RZs2y9ܫwU,G^|Pty.xuZAӈV}vڱ9|.zil҂2VB]>q#@9EDEv`xrzpчP'j gȢDh\_^}u@Zs(,6ŽW]6aİy.P)F, ) 70/2CnVÝ솦kw%g(("ĊB_brnk>f<4Y=EzulD#<_ |>3{z gnzdN:F/CNJ4۸vq@ )T/8Jyω^ӏ`(ڲ{>5ZYqˤE%8$&$a e" R_d\rLupH7zA+L8JF\q*7GMC]F>7يChxsJpO{ޙY02pz؝<ܔf5@oz-*|^]o2  8!Uۿ?ڵ)PsDSRR_>c^<6)Iԛ~s J<3o\٭ax3' bP`b`b6#b`zaY1徱ჷ1qTxD'\fP1@8-dla I2rD}:(//- 1 : B b4/t?,O>":IA n}._2a1QCf?\? koEC Bn) o}>^u=.=EWoF@ @`rF,=ȳz"xx0Y9{f: !by\E|AH~l[Xd bmܺ3&<ǝ;GzY)Q~SfһNrP+Io|bsd˃ SLPnCۗ]Bvxlzk?w4-;0cclv̙$9- 5S_h( ,u>GoR} :a m[3"F`'0[ vȼV+ @L^ wEi Z NʬzfO硺}71QPϖOx )Z`Ȩh#*銨uիǞgR ؽix U+$nҽ\"\^g;40Cӑ[uhMdA Ww?/|n'nY`8DMfS؛ y$ 95@}!Ho~+*pF) ?E5@Y,jе1'=k =rKuD`ǮbY/9 Yҥ|f`Hy觎R|RgNS%b6kFqq9*d5V]1}MBjٹu1O"a;8lB{NŜx=ˎDض+fb {܉IlX:C\#ajjPXB XSU[y*k;ܩa h=t YC/b 4R KͲlW,fBcԖ1 7f\~ޙȤpz| )_վe E,0Lmf +C!Ay#\)TuevUarS.;;aAmܔ_/E|'A)D򖘞 #ּe.xYˊvHRa?1ܚs.].A/ϙ))$VTpƂyko"[芆1uŢ7`x."x88;r3ӹa@iY.'RyDxG1$61[ p36EA ن&xf*ú!JɊIbWKvz`]xMz)I.V\}f+t0+ ;~m=5!ueMF׫iR7vi,22quW1 غ9ȌI4Py\12Ģ!:"]#RTTT6ż'@) K" Y&N87Zi &lVbmhѲ%RG!+5]UP#'mne-T>uDsWNwV\ypG[Du梫C|NW"њJ8emPt;bxQ&uc?pÈ*CLff*F,т$%%q~8#WvUgf@u8pX#cr@T*؉> DN?Lֲ\|n.Qvf*T;dQ>/C40,$Lt;7)"|p|g@tKTt>5ci,0T 㺇z ?Xٳ&c=Ourl)pQS C/3C6 Zx'Qc [uS& \p*#nk|^8kb!dŘDl}/Qpˀ3̓ovGîp6 ~\7ᆃ:fTR…ᓉ _MdA)Y 6)f`ƭ5qhNf` dhx{!'xyBT.բA!Jj2f$ڵ: 7~^ i$ۈNF7Eм+3S@ν0hWbʌipe4ǜozR1W7"or:nz[>O.IIP*(CPd^~ޛ3NN>V!uz,1P kwZ?Lk %En pz(!Q-,yH\E'0#%%%x啷u r©ۭH~*wCCn@B$ %x,I4ū6#7^M%uxW8;оCkQd9IuQ#I-L-4m+0{ޛXgplRrtкXc`-7=^we,7+Skf:j=ʂSD-2Bab9$x85dsD,HQn%RW ؽWֺq]ǝ; RWE*=Cidhxy(؅f1r801=˺u$G!F50na{n#;~^Ϗ970BIwJ3$d12a2HUW`s~װ'eO1MJ c׊e,h>E-vۉW!vBP<<9-]MP9Uǚn*g"j%Lb'hm1HݹbHh. B&v`b/QyQ[W b-(נ_. ܳiCާ!;3ÄԋUx~ӛ wfKR06v fxᣥMJZ 2zp?p͏,hnw7+u+bbV4fE,g3Kcf&<*nfPJ(Q)~䶇͓3@bd?I$R eHvݲ!;9 g1B_*Wc QSQb+DI,T͡"E{j;iS(9l+q~,K ~baTeDP$c0A$-: uԱ9͠ӦǕqRr^ 0כٙ;ZD :+lryZ@bf DZ )`23\0|#诈kѢ="?KJBdgJʑFmt g^| kYA˜y<|)Pv Q<,tۇ {8+1)?%sޫN?Љ+Wa3S8D(QLae%(F4t:eej_+ -:+BVP1ţ+B Yזpm)eЈaD2QDI!Xhe9.2w3Y^RW]Ƒ B TL׿{7եI4Vٗ%^i+}Qv<"y!ApA>([zѴ$JIcH RyA[5rlYz:<>_moĤ[|.=PywFdA`Z=:8@P`[f wrЭgFn姈 =UUv8DYXBU*֖OY/#S B6FZ[*q6ƣAY5S}ϛ}1lۦ=FA&FX1ڸnX}l:h={bDG.\{5DؑKfvkkD 2G4qƻ5i~4{qH%AR 8kC3]/ĢFRZ2xQz eUEMmaDL_s_aG_>3EcŀK-KxZs(ecF.ۨ.j1(VBJ^w6#b1rE]ʊd9ͱ\@|;6Jo #SǣYVEuÊVz0"VwxKIMƩj3{%QsT, oy@ g!R}?x W;~Q q;sĎGBKJQTT˜&Y!5زeSqxà?ͳ M}0!o(:BHsĬ^Z|%ާ [igt["#|øҳ!,? o<7ӓŨX6E[ Q6y{GqP+_ףaC}QwߎTȪ=^i՚TUNʵyjvz~6M Ξ9u0XQȢxğeNc+&ʞQ]Ų"}bk55Kv_MlHa1ئ櫯@'CmPAh6T-i?|RұoqnKq9ABli,zk"]?1! wMI%A:MBAlXkpmHàk.G\v[4eu().C$BK_1|IyR%~w:lQHQ2kM5t\?/(x)رq5R[#9# ~΅i\)t)@9f>:>ҬB p7` g<ܖ-p뭷B U1TSa%D1%, .[&=8EJ3*:Bܼ"3ąG]l_nSB׉̌48Zm+fa8HCq5s שr07s.u\6Nk!Qd?X31 z]4C~I> >$nx E#1;W 228'smلXJUQ__Ӽa IDATϩ`Mӱ-&:hAE2, 6+9GElb:wL2fg(9l).\n^҇I h&lB7GtE$f* 1νLQӱ%E{9ЧNٙ/`5 p‘z5<jv,~u wj_+}z;`sy1dn%^$v=R=sVQ)L=A6nç#⯅'"9&BQk-7w,E8b,}]%X;!7l2222KpC:> $&&{@EJ Z4Qb܈[1FrZB"R?0 k߄rhzK;Da9b4VM#辌ng`3|싐c>KЮmRS*E8 "ׅ*$K+蝷Q]5Ea.GtE64A:#,0I!H>xr2II.Ҫ|fq} 2B]#qנ[L$e@$B,:¡n~?{&>\ԔC0\i5zzםhނ(l>^\dB5t.> GգQl^{\kla ̠Pm-"!j uQHLICvm g_dczQ{frPy:zd` n-+ϼ#/Fbz$IiotW~u?n%۶% I &{ -uj}ot[/>4Q9O=ܵ-6ACBŝ)(C'fbMGq+;zO.ICA. sĊ."`GqB8̌]JzpdԵ ND#AJs9pb'rjhS]CjqugE<_>ōמD0MJrq 1Z_3V,dRr1ï,ha+ktr] )R WR2n: va:΅,A@,!foP%8~}owh_m-O|8xu4HueAM#W,Xw^{养g&T$gd.]ΐh&p֣\mDUjG0(n~1R#RSKK1hPIذ.xg$цɔ/ו|gWr["3jAqᛏ1#2]r,JUq5//6F& vfJ+D\|o +q D:LZp\ $B?eHQHq:#J6__~1j>N2rtxPCI~!j+JMN\z@4ª-at)^r$dنEYa r2c(5.|{7妬{缉]5*8Whcυ"BtԎ{) i+#nT?rg*o6l`EJMn9DMbO og cYXXT$׬`PH 4D*lZjUTBN$퐗 7ヅo[DOA$\VVb/rc33A9^w}2jUC 5%?pk 5#ex'XWr.8LdeÓo^ghu%ٱ;ds`3St Tt,Y88_"kV B$İn\=~=;#y8DI(E}%eeѲ8s@ys{ E*(vG&sz<=+ٹy$$y& 7ҙVc ?сx{;jt~H ɸ~荐hF~ @꼣=ʼn&==Đ;S8%:Y" }0"QL9U#~r~/K \JHש8R`sHwYPRGXn3J*ˡk |^2Ys⋖Ak@Hh(4={A"YfNa)N1Ֆ>dE칩2I4*Meoʷ]"ț}DBVTҌ/J p\ZSY;0=#̖IaO99 h~t:ջbʸ3'<wf4a6n\='n飽BC lI{ _EmʬZC ۋ-Vj"q&,>yUBhpBR*Y`{uS J"Pg̗ }%?tNb疃r|:{g25tبRYi( nJ³s53Ӕ԰ j`b  p({ &h +*R5=~˖א!HFxmşúWߙ,ed wPJNM@,,\R~oȡ؃] )SaqȒ'j׮7o'J 'NoLLD%+(To b'5Mu @F^9F@E~[ !1> ¬I@RB :c%.E"Vku\Y6}.b?B 7|<|KB2%SA080f<;va͉+LV3wP+M0S1kh4Fh`:PϬP.:biH|;q<|<](3,H410V͚R˜PJYW`<ZmQox7*+2 Y&*f=ħ i)PZW 8$=$6 5l`1Iw>~Z w._B^$ooG勜jVWƄX޻")!o7މ,:W/-%Ѥ(y'o?ȏkOWY,zG3gnɍ?CP<]\.߸yƣU.(^WYTeU`(kfI s-,~Y3 7//_{9H tB(I2wJaHLqxq 8SHxoDzt>f7[pSƾ‰cfزy? 3ݣU e=wk]x'2F_v4U#ԴSW^Qa84)Gց.Zk~<>AAhն~pwu]!Ps.ƕS'1f89D,6'eFHK/˥)%fܘapW(4HK8HxJÕ k˵ J:, H G%>4D:0 ᥲfT Ɉ|}3ma\8~lw#p=JU{!]y;;J!Kp _pLW9K6D/V)ЂScϛмsd%#Ops.F aѩ1yt+}V؍:~$=r S QoܘčDd_1*Ywc6DAsr,ɉ0%Ӵ*x=ΊCLfbbӗDΊf$7^({DfNpY+hjO8i\aY*/ dRQ;Lr)ELTC1ؽ}+ sӔ*~0tI/`6kw c,̖3T{+7_CCC}  hsBK Di⦸\m kr*RB̻ȯųGOGwWDݽ-N.9Qi#TAV,ϋCP}.΢[6V@XsqI\z^P*V,Cr(# EVCCcq%!rsf.:/(zG8jP0hHt=2)Έ؄x9GS?H/ prE?G0@YfO;/rO~Ͷ=Ц$SdڥҚߋ`Mz9uGvnCAkI{`@X.5DQE+VDR%ctz;E=u?B֥=4eɢqeAFU(P+pR'bݢLSLY&|8wF<йo{T2$ Ҷڑd=y!tn~gwe6&EBAкx">!;֯C2ѩC ;y)?h /bKTNfIc҄ɸw U#wj? ZL1 zⵛC5 'DvZN4%E#)èD}k FcSɓUSu@2&cM8wg)%+`۲lEGX'V-8{#`q俄2 IDATqA'nTY|}h(+5mHeh0oBǢ_NW*F$C{C3Wn"99 ;lBPL&@`+w"*5abJR4[𯹡iЈoj eF7XD%ѫW0Q"F*j墢 ɶe?r!Rh 8+HbJ9)f*c[a }Js_'e5>Afp @'VS`}!%'c϶ X:c*}h{ T9L=SoKҰ,pԫ[3C3>!hӮ98 nJeFHحNTi͐pׅ]\|~s,峧1y4bW aJ| #F;(_n *#szf)1qw.è`yq'.Ōs[PF -V;%׈z'h]`pG 22mzRo2#))0ƾdF^aJ ƅ;]q&L^;*\0 Hs'1K'z^9cƚ*o63ƨmYp6u6.ьgPV,80~~Ȗ7?Nڇg׎8drE.v6uۛph`D`@0p S`.b+ԽkX+o匕Иe@c/{(R4T(Nm0ZtQ#mD{ Ζϟt: PC:vbMx,X2UG%`" ox{`(T8T(-Z6Ç! kvԩ1S;z'Epu50:Bɳ:;w`XLX0:U"ge='ƥ)UʣTr];UGy^#BRzÇBg2g#XRF(Ч`JEՇ?(=ܸ|^E=hd)X䂜BC2+C8ϣ|-ƍkBp|^lI´p%  cx X6w#b%K3?OLxǠpV̪ gds=vY,(ܿQqM`` ^Q[(8@eǥ3j'&F!}z( ͮ!4Cvd+#Q8^bO"aQs<"#0r`8JW s EOgpׁ/lVa` 91;8a.V _~Z=E? &z̙8Zg-z G'QADGOY3P~,O L@*UQta~iF<zSgfxFn]зK~ p=c& GWä,ĉq.y+z$3T9hhU dzAKjKۊEo,#g`՜|;z$ J{-j\sI9.psḦ5:a"}D>[Mt $R<9xyX;`u#n4dRydeK#jVGujzH7lXi0s$X,wt:#AYBa9pmAQ$ ;ЩsD1d ͚6ৢND_O/x`-8Wt0;6nRߍaM5-9f.! 7FYY!̚<&E=jcڱO'a (d\CB5cvk#2jɕwS61> :{AA>[РPƻqR-a}صyoXۡ bf!ٴ+ztpQJQˤ>gM-[ Uj:8%AMHH C P0t0 ;~7^AݭE[vcߎ88C")!Sb%W,& -ϠGޢŘb1$Xjw]L)VyV^c91tp^#A9.R@}m[y/ZeU-wx3:$GA㛠y]{VB!Yws+5yaڊ 앎 !hѡۿBiM޿Oէ+EҥPJXE*3.;M;qAjtр^HH6a9y ;G;GEJ?{E9:ysh|Xz#g؅็>z?+EbMe{c~D@Ո$(*SbAK0r7k߻.u5hbyb~8i&޾ ZvwFL/WGAkhtC#>6 Y.zd+=Ԭ 8㇎`1"1"H:GQҿD`z/]oJ=D B5!W0#C>+KvEs.LXWV#.ʅـ1 *SX N'>{J.3ſ׮{v-SFZ;;'k\6čAф *"_\PO&!q^њD??v'B!91cـʈ3bx(]:|u2D wv34JXNjZ7nbI߼rf*l"y7<–ѫ_[3;vhV6@"^0!96a_Cά~*fȪE ^~<֎`e)SU"]>g,*3 OG>=!IWJ;v Y䁢T#kQL߻׸٘Iy/ߙ _9F޺!-5c 3$WoMRM=RƤJ-z A=|ىVa=Y' ?A6-+K̜: q I17*j6\fW)ߌgٷا[D`HZw넟֬FFMPLqVV&nBoI\d5m  pEavDvv)Qb ̐H1b )9"p P"%1(: QA;}6q$.`%@b5sf3 iXIPrw"ʢuUA:0&p%j=cSөёh;qwD2ex`"?| 3߻bŊ 4*rRD Cz"*]}_$dY,ƄDt9pW' ~9`Ҝl0 UUś4?k"L!|Ȋi+%Øcs&BAu%60a]>_dJà G%^ĻSiмWT̘zm;S1_/h@惥{Y}'U^Qå?2T5-TPRn Q,R[k/ixr#f\ nZQ,ek[ /(,%tSivg-ĭK6`{ ߌ2^ ]cΪ>_7F =!3&95{Vf5ʪIN08k6Z(+q9}v3'LFw C٢DQ6G(X9 //|չ-FL,/fl_9bPxo+t\ڵrd;:xe*]1"$(ISօs1ӓNdߦ{<;\D39s go,=uǯefSq{I;@VP*ĩ_r{BW*L/,={.a]LCPVm0mAt8r\%ҿ׭;GF]iSm0oe4ݟMH 7m֦cX4pw󄟿c87!OFCEǨҠի&JՖV bQ2tH7F͜WOþDiX9G Q"_.Qo@X(!e-ɂ_6Ehmv蔩2KKܼ& h^N>)I! Rt y'D Dd5jf.:)DmJ&s&ܞ u[,lZVn@Lsi%dF([{"Ik#&2]zv㚌;eȉk ޥ7˽\!4^!x`1ةݾB%uުr:Qh$TǍGwnW`wky:=g tԆG`hEy(, Ww~I8p(l߄g'vTԋu.=naNMAG'$WN%JA/v?22Byʝt{0PR<'ô8\HeW))s/Z$IY,(5c"\?{ Ξ=q$<b}'rBXDiY Ʈ Sr,]|`psDڵq!ܾxׯzEBٌm6͠@,x2<]D0k/PmZ5ͻoV49iriJoy޺5@aS)ˤSevӌӻS<hEдg.Ÿ?oD߯,ELʎmh5a2|eGBS$. rhp 4O-)9ctʞ7|4WȞC+ E&֬qPiȑj͒'Nb!${n4nֈ)Ld]$=K?0lxx"[Wo'4ʹ eE7SڲdZBcz :RQ;9~]?<[:TLenܹz=M0(Q&}0u8,RkWgEinn v.0pm̟8 ѰN-HHd4iXc/ΐV.v} :IE`ĉ([:J(ʀuk.:2Sv@Xg0Xfz(;z̙4M۶AbI &v(5t^bSm](/U/PzM|T(_Re}sq%CE[( /][G=(12[F)iXv#V*gBԘ1k>"ΜYYilឤgb/oܩPIm`-8WTU%KCĽ(lZt %`ͼ(R~й/{~; [m&vsǏHumN@GD"UR2u>qBEZ`-KjfMvJHH*#8eRC3`(Sʕ*bS('$"AG#j6i?W, SdPg~ٻ)~II?z >2@Equܻy`_j$skOq^UQTq=s wn玮߅bA8.y=yk;[[?ɘ,)ČA7(+vPW0u9S(ia{f49dָ Ѩ nN bێ]صq3T3l-kț;ŅO &s_FY"PaS˛b X5oM'$3Ert ًwofRaIu;XT-fdߌB,M |ս;?x?clzrj\n{ n͸w^$0oWuh׋:̞<Ƹhh$-L*ԐTš|Z(S~{zu%+Λ#F:pvÊ?4ʹ XiۜIt; = O;!>C%PVU,_P A9s#KC1lXUg'.6ӭqu)n<|<ѡkgd3P|iA xJ,OL $s5|0ɢޡX=>wb+ЬC'G[h>t藝d}`(<8>]{Xhס} lǨF6 ;i)Q6/>>Ϟ=؃∍?/3KZ* fnE`5 3g)[]<w=0U?  `p#u2b@iF$1w̛)DIV$e6f<lP_ޏCBѠtWwaOSicR4JZWQ?EcYhޡ3ꐅ`WK#4XJB38 [H0WЩ`鞵ߥJWeɅ]ϣdSz/ *$''#&&1hY&aVT I뀹f;!ۙ粬X#߽L4B!߾$g]~"<-P\ #,[w+,fr39}gNecҨN_@wDXE F;X+`(_ʔ,ʓ5TA.ndNNs ޅ#v[nEEASښ(j)LF:ʡq3lY ѣ_?/Y  =QQ '[fEy[U:zPأ5>i] }2N/Vk^Ozv &e A_xŒ%Q[駂BQ[s9Tvqrl~Rcшw>ky(R(d4[p/2?-^b*JDG\Pm+Wo#Y<\-s`߁~&ڷn[XWyWQM>@I\t.~i-hҕ' fhO[@밾H|Ԯq֮5wt?ʚw B7޷ecճlѱcG+˹b89)&7Qaj0g,Q ˗f42 BdF}7UtqDarT2n)kF"%Pn5l;n}-wyq1{N<k(Q<ڶk]{>a<0N8ڶQ?qC_N Sɓgp_6ry&L9'tm:rF>HXҳk 8~*mpɉU/Cq} V& * O o|٬Qڑ D7&dط#8u#NaըBt5pYܱ fX:u2}\ ʹsQJUT,fMslzu.{`nݐllGQ*m-@.[ϞD#I@ *Qt,B%" +NG:PP4*ܺYC=>QSr Q*Xȉ3cbs@L.8ؼj5: 9BL4ehHVBu3x%C˵ IOk6=ylmJ28a_X5%:2P z[ ]rE"Cj撴nHZ%J5h(K޻3G#H|MQ#+'gȟ%U ~@0)DX?kFɢiH2pe\:ˌ,R184w,s ,D.&&uX8i, 8U}ѳgwD@˓Ȣ\"H:;rJNSqΜ'Y )IeAaxNBdR;8;;#_D{>祄kuFR0[!%rP vh-b.!>._•3'qY$F?VpNw{usseM2a@mDQjsn |3|)sF˦ysPFq`\=G0Yg/a_'D'YТW| 0jEƍ-.H-2Dd$ Qi􈊎Sgp9ĿxΔ܅QUWA |nxmyHjjҙ*[o}/SҥKW`Ӌ̐Ɇ4! b,2y0@ccp]ttއ;2ʮa1NzM  Mq`ю*0KjDa!z y(kmx'fBÒ}k ^Ъm3v8fb%%9 L3jTnJ 18w4乵稫`:WݐxYQGI[잉,`hgY:CMbepb,VDXzϔ?q˷ZYl&.f Gx؍Wq,`Rr:B٪Pt  b)slIN?nGByQ~]fa7|uaGC_~ oEBPR(xvdXPTq*G,f<}3'Os0Ŋ5y'dŪDՆCw;^ߓ&g τ+\S2MADjTG!- c@פ- +Wp7:݉^-!>وhx|4Z` \oVYW^vS㡍 833%Ɍd n =DF'6:{,`ZDF1CbEer9j*MKLLFTT"##E,X0`DѶiJ熜%JJ+'!=lDrmE$Žpj8jPPre|T Qq|B>{ʿ'b5ѣ.f[DU>ZX)vl;,dd9@[L`M[G.PNOio53qpBp|U/_o%&zGtyΤH=[O彐͛7d,=ag͟>}ZU95m*=PWE&(p.VN usTtzVEfGpaE )8AװP=S\$XxE15ZM'j,I'7 _`\\78~ jpvu& !k]JV@iY^AjΝ;e)+8iFyUŋiv);8P>9u7YʔL\ɪxקħsRhÒI#qMT9h+wh΅dBY .pvD`l A3 ;zJ_N3G.h'mSڮˆzۂ7UH(  RlDc)EBΜȅbnIzX*{-4RnEl$&}v\uo?dJl @b. {Ken N0i}.PfSڶu=6(R5tn*_ i-޵k׸Ȱ 7=^DE^X̖bnb7z ^D!_s~%!waP|޶;d(`Iݦoƫ*Nv(Ft2ʯ25P528L+DC!_|jY}hƓP1$c&:BQh21|ȩ] /br7B*- 6vׯ{j.c] S>6Qؔ 'gV rEiJ |R p@'Θe ! #G~LktM){>QG HShrȚN`2}3g)#aEp |Pλ$ |P`Q?.?(YO^+E?cX̵ 7/Uk%?Fz7fpwZNҸuW}Ғ-w gI*IS~Cm͘n {Rܹp; {_ʛz\w {V~ηؠPlYAAF"-{q@`xpk߿R7b y|M~ >Xo Or9ލ=JYԆVI՛5$(G*Z-95XӟZ:53ۯy Q_@3aѴaǣI '¢Ϳ󕨂_}Sb5j{_ܸ7o' u7OOΓ?*yoD mN9.#d<r#_(R$ Ζ=%?cg PtȖ3/2* w֠מ_'_7ƀ&3g w&&0Y,(\:+V6JwohDIt\FS\?)]:_ϰX8Kڴ~#A+|7d r.c69ChY-{Fi ع\s8RO1tרYKŵ{<K|þGO+d4n$:-!-^ ˖.ƹw9uNXz :^=Caղ{d?HZ6&Aw , ֣F_T/{{o:[N/RBGC \Rȓ3zk.3ǰynD ,Mj?luIC@b(Q=$Ύ<~x+VIMl3-O6˗̗jlZ9;<.Pu RFu7FlG]Z~\=;OA-W Aș5+]sg`8~xkV8s s}!Z=\hsTXA~HJL8l?Mgf y_{zUJJgV>JEՏ+"׋]ˇwo`q?zwA>}EB=߹'ןp5􂛆yII&TUʾ"LA.*\ PVhѮp|=roWt!_ Q7*myOQV>˧M׬/ǝ'кˣ o/}3~ ר7ؼ a$/m ˗FY^!JcdcW~BZi &yU)ּ`A.cm4>EgIDATWUrҊ0K,E1e$|ٸFҹ4vZ{|M_NdڵB*_ܿG1fG:pY*#&ڤpWVZ&+ț[ψӻekif$,7 iU|Lf}l k)rp[;ϗҨiBA5Ƕ4;k41;#P 4ݻcE[Eq4xJ}Tsdi-j0sNlD}i.Ro;S* >(K2>)W 5މ-g {P8bex -±oMZ J5mE(ȵ{|ū$pdf6RϩtnX~,'Zw4g>u*-5otCI?(֩pl[}޼Pި z[2I~=0;pV5Jԥ߿GA+-Z%C r=RU// ~]"WPJ?=ECC 11h4! QwjBo" t<ĩ3?(HF~ם𡥪ʼnhg'Kf_cA]9$e[!#< _bME)^_(ԮO?ʕ#{H@y턔piD&I71[]gXT]#*"JHJEA,"6l1ر>{QqY4X>%X06("j`!Hqb({q#=?k5}FTN a3m ;:Et) 'L.9H. hٹ?bԽH:Fnk#53Wm??~̑)xLDaЂ khI3E: t3ՙ학̀A!p@ d7;ݽċCd3PP$&Ql'Bde +̻ȾG^^.b,NӢs$2o=vNR`\5y=q|b;S# `w1鏼x|8 rPN$X餕^8þAMNhQL,OF#mt팧9w ~ۇ"Hst& a"u J-[i@K :v —H`w%HM˗p)495q 6.+E_+Kq۷iPpCh鬵ptrWtuIZHg`HXz>K #CMۄp!?iy*NG`U?tӇn01M],% }غKnh0'E ѳD3;~wNqHma%@Ozf-NX]Ä{A^bFv73$&DC_BAƘQYaW`OVkv!-Y%!HC.-t;|?)RE'otLZLԋL.8_1,>_իSͫi8D+do$KPv+e >yKFxhjHÎմhe M͕'cS[?zƥgxjԱ͛} Dz ӥ@5ٍs3#R6 pzu*C0Nrޱ-2]5Z K*tHHιl/XG2̑j8 {5-tm:m򑄚?W>ŊFLR ,{vNN%m9 LljXC>M:JQ&wAVyR n%$ aqUu!y_Goio7m9w)ԗ9c,Hdh޼۷H`.050O^F߉}4cd'o{ |%΄/] /(4&l~1wl<!HfEX9:r9uǧ[Gbuuߌd޻/2+/kjc[0u0sBa^۷M b*$mmDU zYa {Zt#<N`|-yG}¼s ݗLl} 9C^۷ <2}d?(ӗ24B$*~p?׸㿰=mR?zhԉ(6Á`E=kFZ'="m|AlEW(a\ B@nUƜ*u biH5hIG@S%@7v%5)ؿJJв]/$zj~c1u 1z[5GQkC^Qo 26o YcP^*Y Acm<\+: XCvxkLo#Y]-ߡ -;+h=G{[q1HHN˗-NkcY#ϒJyz `h`}}=ϟ?Gϑ'UZqAx :w:׽{y35TTT/=8#^y9>#qذ4d++"nsk 4FFz(+.U{,R(0c{c= B=:1nhۢYkdtk[S L~O|ָJ삮_+!SFW|*}^[1s.U[$1h5:K)2cqMqh/ jidm8û+L=GQ`T]sqC7mAX`.i~gB0VI.f['WKU6N($DcFHDYYu M5QvJ^u[/-:0e:y>T-y7맆6 ;G89ŀyRj9 B0a@ [a(Y{( H:u Sv|x1Dx***G3O :aWS3k5 f؃';wERSԩ )xS\RFf|hhjO_ 03T)ޛ΅^AR P^^CDхqOCuP{̟tIGKDNn.^@S :tC +޹Y~/FkX^?oЋ(`[1;sUbcu.;jNF:=/(@yo΂ol$*,b.f00xwҬ$ƵDT\\55>Zn Sr}!e ^L(@QQ dȄm= TF$'ѫׯQQ4o֚a"oCC`:qi 69!u A1!D5ܸZM Mќ!D5ܸZM Mќ!D5ܸZM Mќ!D5ܸZM Mќ!D5ܸZM Mќ!D5ܸZM Mќ!D5ܸZM Mќ!?DĺIENDB`dataset-1.6.2/docs/_static/dataset-logo.png000066400000000000000000001322731445346461700206420ustar00rootroot00000000000000PNG  IHDRax sBIT|d pHYs.#.#x?vtEXtCreation Time03.04.2013tEXtSoftwareAdobe Fireworks CS5q6 IDATxwdU?{+tuf 9" AluE]b^s+ W0bfIaR]8VU`y[M{e얧L`^ܥOZ:\ws<5Jn B}"DPJ5ƂR9H%mJP 9-O 85Jim '3Ux[U29/Oۨ󊎯[9oނVn7kiJkT7JS+UgzJ+Y;6vDɟ-{٭ !h,9غe#XчwOte4 -axٞsN5袿T9]zIL, }7\u%w!"emL@5&T=s ^1kN}"?O.!F[}weQxsghGzܳ[qWoAkmc\qQFcpXb 9R ET-FAcŢ,gּŜ >*]{7pUW;Jŏ JC)$L޻s+~%b7/ i(AC"E{}Gp-]_Ab uC5 t-tï~%}?V0+@ Br=K9Lkkt*@)PerlIn@XY$V[$vAqJA69G\82(yV 2! w|O~03r^GN-g_7(`c?9 ܷv.'޼~P Ctt572$ShYĖq@ⴭX,/Uظe;*F\t#=C;Fh(rhqFoA;A+CO}v4fޒ$k[ PlcU0 mW6<'9]/oXboB5ʤt&NЩ,aJ6?UVF)4B r^r'Ch'k,:v"8[! mmm)B\ex݉zK\eo|;gzG>;F-O%*E7q; ?C EK@#=SN=ΖJ@g ;I5YbwED)TE3c'GS_([0ݺd|J!N(p/}tˆX')084w.ɡ!9Gz[&f(&qOGkS[>]8u]_,J"$. >k9+8g ^s#PXGFc!N"U;+-~RQ)M3} h"q$ \T[J!Z!bR.Ԕ񑓊@I-:LNN3<:]cM7 λ x5;&c+H nEa/KOq;yCGGʑ[ TDQhT%5 xe 'թa k.NÍ'(MU(AŖūaWT%*00== eK/ajdSp /佟`^-sdW-k\(R['8sd:zh^7b$.BDl*AUJD8V'%5ɋ<5NqjWA%|}̣gOn꫶[QfAojGm-M@ZS*bt|˿5"R c%XEc?$^A_V$1ACe QB6HQ!FYJG8p翈TQ'7`❺!w2Vߪ=t\w`TuJ}e phB\DK}|v#P6QN\D`tiL VP(ѷ)xt3U."LSS͏Z,ŚʪS)тq>oWP---t- /U6h0"eABmcdd /otr57Vd+B%4 Gl:|lBkE_ p MLbXi$\&Š-l!J (L Q4e9Ay'NQ,U߱LABkgZyF&ۙ)B=.]{A[{ T-VyHL(ު8غu;J?k(9\=]DvIZ#Z8կ攓pDʢ1,Xv0ɡ 1[R1U-ۤP;"BRako{J!_}(85t Ȧ]p.BbF?Ew`Em/1r se-WD".~ױ[=:DX v\-ar,蓔&'"<#Dc|͂PZ0((3cc#.dBIzk-`5*6k%$!glt(pD$%3,o_/qWk޻'m$D~˟i(6 yIrB4,様0+L1J9uꜿX@$Ke| $#h(2YzD|DTU88C*Ф)s^sttuUabzz*_{hz3Gwjw2)e[DD7Tf|EY+)`u,gpGH3`bĊuNeCiElcJbcj S3 099Iis,kOZB%tttєmF_1h"g*__*ʴh]oE|[S'wkͼ Ʉatڃty,hk8*T (nH7c]@0: "y<ݖb+LO19=1!ZU_PndQ:`T{eڵ؊&cLtǜx"G>a:K[[-,u ,(Z[[wpOFG兯xML<D\?ގ]Nv 3W}fU=\C"&ET!$_Q5QA\}VG)=T*%Z[IgҤt3K1"GFٱu\qt/[΁+WTKU{m!kAVB@iʮ ڻ)Ǚ(\MYsS_0fמzePy͉1a*'R@4DYAI_(myJj4gHq(*? #I%|[@8j%9yh3DT\3 Or_adVPEb%-ݝ'&Wle۾'}(X z_k7. p?N|ٙE/ivN%q)D8cC'AΑù:A\>4Xa@ Q4g3dcVWUVAcS}9g+ vz~z& ``p}#\bysw 4+ȿls? [7>FJ /#-B4($z4J CNLSg]&FQΡ\zGaGHϢ0"0 (ZC;ҊH"Y*z+h+hVi̺SZ7x$8 PgَNJcGc Ӻ7#EXR=M@Ɇ7o\ yɋ^6`'ʊ:e* .BUxGε l{wKĢPb|Daz!\E!˱dvtuu133E>_޵w}ӆ>xV\*I+x#rbrbP7Xd}mOVjSB&xd&V컜,2-x#rLk>p!HTUL<ֶ6g_(OXׁf %w0>4Xj"-gP-[EtP(a\ zU9T/_p۶BB.Z;)Q{6˼P.9g@h{w}%= 1HROu<O u.׌:T%`RaU.@,Ƥ<:)Pb1Dbrz Q 4ݝ$*S6?oFJoUN:'?FwlEx;8uo,'P%0Xb+ЁA Uۘ? ƭN9n[cqPsS1>##<(E+GjRWUs BbE^a#vJY":[3,DUXgDqsճi5W5G@[VD4Fv#//ϸqX % % mB3E"aIς׾nޟ&~rϭֶX9@9+9b~,xb^th8mW[˕8)*L3_r ډY%?e^djҠ 'hx%1r,]=ll+~}'hgӿo8izJ~\`zjﺋ^ErM)=̈́n#bZH*DKWBՏlS~ ܐxZbǺ@S?K"8x?_]~~= [@h'ĤS"bJ[**bCõGl>fᝏ֚sV 'r/ ?{8wdh] 4L;眷xF"\;.)ҧ㺈YIbr94 l=F e-^uއY.v~ '}2,.GI2d,Npf K+ - Mo&Q@GXJ)*6}\_jxIQsmZu67C>ŷz}Yl)"l&Eǒ "DIxL"zʐ4έ){1LpD#Tѡr% ,\{ Ē\H6)ۄ թg IDAT/]Ǎ?6C;?0/E)EKkkZU+R*bbړ >P!U׸ğ{?;B)Ny>+oryZ)eQ_Fpb83axh}'WFak){2IѴV!a5y| uM &z'n_1-Ll~'k,a&?Jud8-DQ 䇷QL~` o ') m47q&>mFI TAQ~! |ᓜ(cMt;3Ζ 0Wf|\cO> c;eGXAj-J ~nZ7 JܟSŝxqW}u:xk뉒[cWC* ~-b|qa#eTٻS!ZDS lbtCLl}M2A'q ) ?Iat;ű)Q$? o8Oyj(?O]k߆y?Uсʣ*3ςEH72?T#G;rc3 ;7^@PXalx$'"2zbfw kʥ}Ŵ;vjm&;NjJjoZ8Ӹs05-s$;Go dDí7iP mRB$&Pqa< AS~$NJJA&H-VF&Y*{ -('LO?e(Y.ZU➇yKq|k-&BIr\G۞xw1{7TCC Va뙞F-`4 s}+htWS6'5 ujVXHf?T hW4gpJqad`h;UAO}tv/w~l+Q}YК!X+x3 >}w)%'L(vlH͢z="l?LjW˥,'L:B*䎛~Lą4!P+λFFYMwbbggiB*߱ܣ=$cXJ4y|벓4,,~72 82FPa#)v0>60|_ڱտ]0B.[ RqIxqI V2U6<,7tJ㢈)Vn78жVYVTftVĎ&;3mhq>~ _yPhZف:Ċ5Z|uVpSo;ITw,O};*0a4=K|R-U Up"p-*䆻136gdO{דg܂4f~Q'^S|P<1}7_ 0֢f?IДYصH׸$z0 qUu8k0]KRJ:cJ{0k }!̶ ůwQ')m;&Pq ez)>Ov1Ht;Z#A 4k,-i|/O|˼Qx}:5iѬy8 4=X^fARBV8ӻ> y;}j1HT*ѫGQ-;6!q>5*|Q ~vbeACH01mbydM,[?}.^n[ +6$v*iVwx;w8m<=w(X'~~ s+IEiLLqoMgedEa?3,\ǟ}|ףl޽!ӄHBs6qP,~xkXw"sǦ9hU !j W>PJ;Y133C6^>@XU]B"'M>h%rϵ6]tvzK33 F̶CUGTK @&%R"6>( >VhG9UU2sJ)ښx1lݰns: T. ͝fGgZreT 3젿1&&ٱu3{B~_"7lBVX*pQF9$1ܣ 3ib 1]ru qA( .N= ;0v,] C~suL032{86t-])FOgEܹQjswr|m|/C>%V,^ ο$b@?w};x;@:Ff[hB*XRկhmw$6㤩*!^.'{2^⿡5ӄjJ%@.~m.ۇ[:drhg,n12G'pV1StA\\|R*iQQ^xEѬ&I]˒%*e$K)nAqιҕˠ{+[Kf2S(ʁ3 065ť_4yC퉨*j&~ɝ78%r[2AL.CWWW=^3Q JDID٦ȻCqz~Xsjs8%-mP."<։N05=MkK tww|_bq?Ǭ>%P9*BDZ\J[N~o=9"P 4ҙf$[*8s5W|v'bD5(n;jAP)ݣ|/t9$n8E.({**"dbP sZ<ÈPEivr!9shjn1^_Wr~?ḓNf{!: uۓ^t.bG@ǩXpCф+N A-Pa(0IvL8 1yGz$K4ۻ#2]@ŁDrQł8nd?)xΣ'X 4jL*C.F6DS 3u .0"ݳ,8{: "bikk#0 RqrN48p5TUrf}BWG|q"c)=PVw m4UnZZ9Eq^gц0 *%"_SBT"1Z>/lMX8"28t S?r)gu<ah^ Ep[D/ti~sXPBl'0FѲǁtR:"mPOM;!z}oge>. *RߌDܜU{ bjtHKKtlMB.o?zK6XK9LG:e1m9Pd2OBs~ŒRE h+ 猡x]_v 9FҵT#{H#Gc# }188Hoo/LD㬳h[g M: gI2mKh^7>H`m\2P qM9N5sTEV>J3.mhh̗orm7W-,mA+Uw?+[rK*(oE wqC b8kV*qӘ1T+e hZe*hONgr>*Jy Y?&^A}++_h!.LXK5qWs-L8< S ?0ߺ{|u|[ioL?ļ[R"So@k_ēؓNVlpv I@PˆI Z&c&D9Ru Ek8gQw7̥ͭelЂV@kl){¥ŹӤ¯Z25Dyb<o#ZZ@GS.S f疛1Aȹ}=K"!JÉ *_q|JBo&*rFFV!*@#|hJpQGcg۶]Ep1,\ccccYaDAJ['3)pVZ Esvnjc,LP.hjdcka\ Vjܸ%Nj4Fc}V*!֜~<8 "HLtE~|~=3҈31]i S YnYw?SCC,݋ Hc ]z-zouOtrO ,cf[#,@x29~iFFFP(⛹B*LcLHqdՙ$*ƪקZ~+HmG[':&|ߐkV,H/v~+-կ7#|waBИA(\|Dg!wP7t;]@A$vn~wv}B-J)(ZQП@8o`w#7bp^9jVDŽy&An@ hwt} IVw$݃] e}pϯDw_YxoIeRXٺI_S1) 8ŧRG8KXu5t}TYY'3g=e+HOHjoѬl6˒%d2)vGiW^Dd HRʍ1R^t9}4rI?y=gjL_~@〃szG([kwԉ!Rysy{12-48CLsl:ko @K>\7 ao^ [Nk>>+Ѣ*@JGAR*.W}~!'Wx%/v}ijXki"!I-KWJIkk+zC'xضnel- Rr̶mlڴQ9D* 12yJbWܦ> Us73M9u0T1|ӿu-ӌWc1>$'MsѼo⋾R ɸ1KJT*x߾Sf^ɋXs'WP? CMTU!%&l3f D%ȒÊ_]3Rgޱ틍rD 6lb֭r97 ZLS) j]jí?sYR!_El\>; B0<4 F#d1ctuk3gYXcV yڛ2vwh0^µ*!_pUs0?^_y5ϯ\`Dmm|%tlAO%-N gF%RcO~3>'#^(U3y~LS*crc"PUCJwk- l:&Cjy&&rǿ~D7{ Τ.M&] O?CDXFr\ HT|;=]d D{QO8zaڹ\]MVso^+ȮA,㔦9 -Y2(QAcABKMZr==]ݴwC^|~; jʙ俧ZG!@ڄ]RAJXFF%h!14VU|ӟ%IO~{eVtzM%KSG.[NGX]Bۊ4:*XFɦ .~ IDAT0(c7s, u2=rϓ^z۸KM>;׮A*v۪sd"uKO綶62 [sdz`}kLٜxꩴgQ"&eHStJ)`WaL(Rņ@ M5c! |_ӻBz474N %[g>G%y'k>FR 2"L186Fat$*{Yb%W,yOX%ZE?EXl|]@JvMVoJBr!SZ;ZG Ppww'7oeV>𙏳vFn:~%<0,3eJs҉Kp~}1$nkia"6IY30Owƃw݉V}aἹ8,"655V 5k+ڔĎ9hji!~1hkBpb"bX!fBl h6h_:ȡ@!D#ryor5KPp! 3~sAiH0ؓ_$tI%_ g1XB$Df.09j;GZ[:ɤp ]iqy"ͳ˗qmѷ|}˗[!omwl:uƌeҞ4O!1 v__Ngabp <%'H?"a΂^=6+eT* *)P}wƶn2YAJL]0k|5TS ecN/vݵk 5ٸ!B!hA{Sˡ] /T ğXRUK; 1H%hmn8ȓ(u+ڳAۡRR11gu,[ѡmDn I!CF`-Ξ1Bv}E#*g2U P'R/u2v_4Q(qMWkYK]6ދx=\EY6[-]՗]F^NvݎXWV1(qtj Xax*0,>R)׋N3HmFcy1P8|- F466HDQDET**u<R2w >ϰqC:Xbr"J) l *@8%0ǟtPȏ+eP Q/6q-Ynjp[N?SOh#|w;/ j8N"Cn rahlK+ǝp,֔1FZRaRBiz"*;EƓd(ΟooJ)y)_L18"?#7\u{'u" ha{S)N@ލCHJ3O/cX$DMyh39#V]v2tΑ&e뫟YA+8GU*_(DZ*'|p"tڮAS5&)՟+b>\TaIchmlT*UeOw4\p9"ٕkټ7oT2F901Oc]TFF1V)F+e IB,9/[CkG+HH[G;i= xq\[(ѷz:x,ק<'z<>Uq0aj})d Aʸ%cCEGlĺCkM{GxBW261ѪF}SkKKNMES(eb,Z01QV?#Qp3fNUk!_dR801:B\V ǟDJ):[Qh۰>,~G|`nL)Z_zYW70c.@ H56= ZJqI ضn)>{,t5 p /SFJO{9N X l$j(GcLxb4e:t8)3tS= utt!P4~9{'*Yp.:2 N=Z5ӻڱ7s7_kDn^pߺFIxp嵼b%W\=mX80&%U1=dp457P1Gnj/̿d3骑}y9Pfb$ %ĉ+Jtr^11JR24GRDT.DZcHRtPa0 IR(%ԏcҖ C;ghݿi-a*Eck+8Ӹ)wu`,zf3y=V-CgD>7Fel+ZGB 3YfcApY|[kdY(-`4v0sfoH+lx!C~sܛᴓO`z :IS6,ᄳFh٥?!/_OUy5NP"$k/ZkWhMM u͌L볤Ccz kVG\,q:QV< VBN}A߉hijēGX_mXކ(GVX2ԉg~G<-׹,&kxD$Z7޸&4ao> 5^D#HOVze[nrǾ 碵8iss̱KګYj5BS@N+ Dک#T4N_$J*aNj2r5tttrIr7e˫ײu&,iN84:X/2QWb,=aG:;|hH δb2~3~% #Gvj5ES֍Y(akiFcqb(m9Ah Eeb^ҭT2u(^'_A"Ah4"@ )Uw`8J`bN9?2ъ,ӻ蠾U11J$%U_NRa)YsVLW[31`b!_󧿅;[~KN{l`W@,.1hu||࣎7U A]n(u.X.at "8i( j!Bid+WFn+yIG Q^"+Hei]dAۚM \i*cuڨ뚛sc4HiZ1uR!t@ЉO# @dJFu4BM]WG~`= 9#kosߝsX^;'I~s`Y} E{/cPIHfrVb51]gb.buxFE Dz;Ls7"SO䍣>dz,1FhJ&Kc"V&21XI5w1goN! ıAGRXRз-} QP0{ThtEz-0Q^uտpr sWHZstxš@?3\bW 3f([Y`J%Bl>"TW xW&mZU,$,]mVB:v=BO[XIms:&h0>l^A.LFQuJ&,[,A73<:D;g6VDN:<0Q#7v2 9>Jq; e4d 3cy&!܂0砠 hO8§0bH08byw$p\k_"f޼݉e2`Gw~ĝNpx pHFZt%Q;un1$žA -i\ K0JnLk/x9x'^jbICJ>o%R>jXeI ŜND} ؐ/Vhiicb[?|T]P믹Ϯ@0#*D%%1uZpү}QbC5;kRTA*nfDyPz0|V0svI^@ "Ye .tQ$'$㯰&C)"Y! SHc&hғIۜf璺ORIyTOΤ/-F8O0Opmuk !۲!>Ѩ@6l\.Ќ9iKkr@'t}3[a*C]CEŔ\M kw`^}n9aB\Ϧ҆cz&EBnj:L)Mʈ0uX {. dı$oRgǯOy8Q}2@Xx&tT&6 T\a]# 1rpƅNA`12ə@vչ;\ضGy1BB1!U'(*  n(4NYb%W<&)HPp47w $T%FR%=V 0Y~ݵNX,_wHa>8f?Tҭqժ5` Ǟ|2VX/BS9"mX3 >imd#J]|~OP%(؈fdQ uMFq +^9GLY.tVGJS)W QRFXK e5VH& a%|cr@@ }=7HntJ*z8H!$)ڂ{J'"}I(/UN17Z@}RnTH.O=ˀFRJ,[ Gzvu܀)XR@xoVa9/9ǰ0 1O*@TøFq]SHYVn$tSTD#,P@o'z}`-QRX:gm@ bL_y xh>}.qO+*]7$r";ǃ`h[?FyscTZUM2H1yAgQr7"%pO_K F#tMV/BߡP^N!6\dz\n 53N+jj !.$~и981ISѡAFư] z~r*R n; $.'ţ×uǜ|FV"'MWۮC"e,N~;9\o@tx_SؼjG `^*C~p@ EmVǸ@DcabD9fUt%4u1n&H:ib>^xiٺeh[wMڱv nC'TQ 9w5 pq:>v.mpc_w+D] JQ,-10gcy3oZvMM [8 e-[ƓOu?oW8錳B۴iQ{ĕ}ƒ*۲/q\|դTz8AU}ERĘ"B:M-[ g4w1<6qazaDL&üE{rɦQbv/i0C1(ߢ*VFtEWCt*\4| >3%&v}U,{ILh#^0sft#MaF9?[Ԡ88|!|v wrXG$[IgKrӉ؟EGV+D`hEkw'}k+5𩷽]>iI088@ ]XA+hBҸ];m$.DXAJ5ZH!}!*J<_vÛ6Tևc8+,I>Ӝz{.T-mZ| 5_4H4qRc+yXjm,90?Pl'ccree<@I ﱐE,R\!VRYA D 7Z~{.…dكno3u8lu^{Zhhi~{Ν&dȏw~5+Y_\=/v*5>aF9jiy0ܚqHCC#,fL(o!60<0(ʼnrɾ΁@c[ ]-m)Xv#+YOLws+R zc0Bl Dwyr(L=AƔ MS[;䅴6\ FZfA}|\UO>E>80AT*Cc&M6{13gw:fAC,z2{mS7svB뀁D|Aeݳ5.x[!}.t{%@6˞{js$j _bg4wt1Ұ})jD$W^Fؼz*nP@qD0}~$ QS!G?ᜳNs- i@.ںA+|>puUZ31C&@SvۏFR"MXFEL6wл Ig8N7WUN;-[˱m`č#K$6.R#',Û7n)0_sqw S[E~ރh CGq:wfQV ?LH`s?z ƃ!brÛn#hNCk'u\~F8Go @ipWsgsa7]K#4kQΙ ʹ7eH7X ^6 ` nIcbT hAs&]񆠧/Gb-4ޓTe}`*<zW|Ɠ. $?شj%]9 ٬k7ᤖ@d;fr=ԵϠytm};FӬEM8nO^7$5*$};4"dH,\628T@- IwQi3]{N4kv\=͉U;\_O#A`9}a(2K Tr% Ad39{7]XUB|}4XІRT *ֵji6ϩo[ړ~q鏙߁İc N8/ ch!"Igml #|NV=LM?ʔ.[[N~sHğ)sdMSȗr2"@P.:1BZF=.0R!MD{7^׻n]hTIl5|:~WrR;^[Ř,==(yOAY5,>t +J@ |-"%' M*?z@w^ݵz҅o8 cc⭛[S}4 J B$-}[1B|c%"m"%326 ǽ^ @ZX(%Mh&n#v`[%ٵ)Kb&c#R.f}gJDZd+ ߿c*ȸX{bT*0oFg֚)$XƱsltؒiD93@*}Q,`)1 Uu BGRrD@G1U/R\IuhJ0cD~C `j,c}$=1Vy D@RtTQ>ONkR7Q)R`p'1R9@qO~/ HģxJxWJMpgq20o M&BkGI0除-#t0\L$3O om'7 df3rقm+@ZhhkGJ:$E@aiVgiJm3,+];@5 P2g D ShCyʼ $ PDK2].đ#SP,q%9ER+uDcs3?x6e,q=9XiTml?ħJ\,#8qr#O\E \}"Bu}&۰ NbRat)0*5%bgslMJ~i.sƹR!2tګ:*+YQdAByҙZ8_Nsrptr,J v`'/S)10R.N{l"2}(Di%cH MN,OĐق4 `[#o$rlīHO׾-ü!_|J%o#hO>C܀i$4ᅲ~\K}]v {N:c#'<ȣ-kݐڏc@8@$eR 6111a.ǙalJ8QL6. C~G q4J)Ga| L'YR l`ƳF>XhP!2H'#C~L+$? ͛yh&Ё`U@T4!TLB.Wҡ_3؀ Lҥ\ DZI %㣌"-[6#NAKnc~Jnɷ ?=?<:.ogWhqi S>UdЬY,@ǮC('Gn3c^?c-J ~~"r{&c&M!XY?V=ٲy3L=Th)d¬;9E͆YU_Jo#3u,݋J(cFϬXɶ}pQ<%٠ֺpZ۰@[mTL!UTK ے*LnΨ S4G匸B~  XD300+bm/1".P(XV?|7r=G|G:H^NA8*s*v_GdPL I睕ƶahNW2^7$P+%&n$ qttJ\hyk?{ ~?}I>$\OsU+_֫Avrn/O'{7jrj%;Epp駟=\g-T_ݣu@ \z"+DVϚMP_GXƔJ&*pѿ01%IalYׄ:v[ͱ`%ZY0C l#343D%*8ήviv=&JBxyFԵvjhu+ٲI\H J<)SV (i"pF$%JJlB(r0 J lZ4X<9SiHf1U>Ε?!V"=xıNXPzbw c 3(lojlADEV rJh`"~mUgLO=#si mdٟF 9}\46:u>aptLL2CsWU&>B,W/%uucVc'ݕBk *PP+&R!qq8aTC;6T:/0` :Q@%*  y,@rHsB0`4F{ #-}7zK^{YN:|̝ϗ>y!\FH0I~ 﫱vBw8aLGza0_d T![/+n,}1m cBˀy.&@~FG3ڥ$%LX~_47uuY$3 ӦHT&Ue\8 AmK~o5$* l`dRF/gbr[ד<ؿ5nZm\N= DD]t @*}B+Rytex&E {JQJfpއ>F&TuNw^x M$h_T?w:d\q@TTc~%d,p ڸ7} @2>:DX,󌎎dfe2aH\or$X,9.W^xa$ X&H\ȌCk&kV>R{z< @Kvay)PbkMr J28p %ZX6,[ $GhKk 㣣di  e5FhN>mX[Ɔ>Os[GBWwBPл|?$J]C]=yn B|r`>F svMK(M066VBF (ҌMm6mfӦMlٲ&vtϭB ~Bŗ>!JroGZK 5/H{ XGְ%$Q SGҏ҅uBaV_8`d\H?>DVVp%Fs] -@xD-۶{*BZrw|5Z9쌵s*vPi EvlcxS^lՊK'hJ{k ̧'/ )!0Z)7z!TORAՐXOo3q˭4uOCK_FHefӮc+))*1ֲ~{X>%hDxCf2SsIByxaeEh˅#c(Ql$P!+[ϭ.{wgX±A@'7>MW'I,wyi˫sD^l~cNi̱zgsɽW kmd^Gl+5lt 6!8 Bx-;&%&=VrB Vz;@ؿ/xS[Jy鬣HIRq,J­<琼@1kFF8{pbrlQآZsn"w$QdJ>Ʋo׭e#%/Jl2L a0F U@8M( [FWs DZx=ihk!7-[72}ڌګ^J3k!rT]=1v" %lcLz6vmG$x-c[}A&TNmI*L{My}=XǞr&Y߹L1 0GE͙+i`Cjx;pՎ fX MCyU{L˴F $z RD\+*.4 JB:HI&O{=k=Lca|N2}^{IiE%WqGE&l `R #UQYCVJ)5 ⼼8 3̱-.`)qcUL\@ѫش# BٰhCh/gPnG NsQ?7GQ<-UAÉ_y)|k"QX Ӛ;lj[ vcj7WQXR{975˖ V-**Ut"C< hq쑀dKPOl[fe*6lLՆJN:tiMqoWXSm-\\]O:DMA]զhclw s+/=7_@XKD9TwvB$ԐjdK#ƏG?$TתXXkA% i{hMF|֮\@⋯;Zwz!w|dDCo쿕ʐYb)Ja8ƥib\,zE >bG""TTlDkTKMm45ˆ, dfJ}E1gs0Ismw:1c%*r''/mP Uu^&'1]NE안P\\m~Y777y܀=T0F\䓢sўg>[4Z_A(,-E͜OusA8ϪA-Meh3Eo5rb֯~Clw QFE&cb4ThBv8lJFPl F%$xbg(..>SVmoo=^kaȨQ\sŗ+kuFyXОRUE["I*D(,*.G|1pmUb%0l՞9XMe&[L2ISM x~76yޱ D*(:/xҹYab~`B@e$ԂtڞCLv$ Ug?jPqw=܍N9z݉ZM-Qh[Mlb)}gͤ*7Uf˜sQVFLɐiӭX0lXpmu (֯_Uyee"{CtDG s!%vF{׍V9QE.EhRXZ;8 "(b1'riG$ 5uby'b IDAT'ϣ ?(XCL?/4+>R!chjjbƍЁ0thbjސ+U;oe#&{SOg!( [qFcJuxoEwCHv" h݄4gͧeev;:;:@{Нw_(/)eĨ13'ZGZHhw$IlU4eؖm`n{>Z*H56 4d'|t6F)<Dv߬f6Wײb#[jI=kwcT|>C: 1(;`9sKbshXj|˴T8+1:X2g@:zOSF+X7X !y-:.8y}iqWHg@+ h1c #76=(o}]6y&6mKE4˖|F/,`¨!6<8߈ˮ*m*}jׂA1m3::(g''ξC&/e̸5E R}RԠhx{H"d""5v0};`3qٰz~x2A9i[-f,tK/e'`۳1]Q$W{x<Dց*ox?X.eL0jlNN!T;~ y )/Yx)7ifZۢkuS<ꨨ%\*<%ƳOЅ3FswԈdⴙv^$X62h ryOib}?(킑3C OX J?Zs_nxlH5 :C*D &!BQP7_hPp('ƽK(ߎ/o;_ŨlcR&Lb3pFbĊKw1afuȟNW3vF G||T\jHk+/e }_6Qf{ձӔ]x9N h l ڈ#`x6TcQ^_bhiN-C>F5Ŝy6nZ(N8cW۞πٿ|C)M>)1cΞ``v,phVKv',F:L]֬{hxyYYi 8D"A" c;I8Դ޳1<_ x*"j7ꎝ '.Ƒ 8ZU;nCD>,[kڷҙXV(`I gäG_ǭOʔ]v Wvu 7Y Q:yfh1ԯ[ύ7wZr' 4Ym8E)Fe \steRJqDhmn4= .(o3yI$s(el&. BGL ¨h'O*eÆƝղ!}K[?1={weU2~{σOh^ ϓ !KmPlEi73*+*p$cK{{ͲpxJ y 1VDn):k J~j|H$R4 k.by$9:tTkVˆyXxe#E𑠺a6?p|fٕc{njy]H$vt tG }%T*C{"E* h'N:#3v0}Ȳ^g78cz0Ê MM&GEa&3HHck/'kb[jѣSPPsqfZ eO`.7A[M-x$?^lڄbˣbJG k>@ITUUIr<`5ZALs %X|Λ$qnxqC^a; s`Xe0'oyGÜgk$ʶ5b@~Ə||eaڡڦOL9; (2pM7s©g140$7_rQ#/ױ ሶֱ'c:pBO\3vb7|rV,^HhTwؼy3D[ߤFsOP3$*PUXE8mͬ}V-YȺKI6Րm&cPi3t2JJp'^ *]*̊"KИ(^DcCwIS8/[Qn4.ʀ'yɧbb8o1,/Zۄ 9(PNfՋ҈jL0|ʮw >GedX03 dmd#F0t`'$9xիimm!O( rԱY.S(dǛK[/DLqa.#$pd !#2aС 8n&ri';mv SH)AŭCk18S?T hTYe|y46l2/YLZh Zq9܋~ں;ݎ荲ZYvW99jLFAAH>s*IJ}ac<-[_!UmMYb];pgOXO2i4"˦EӲeؠ,6T2k :o&;e6 D6,|V,'jk UJ b>(;$bAAܐ.كv2۾Ǚ5kVҎϜ@jjjذaCܭj:O.΋> O7Z6 _hooeUT_͖ĵОΐljz# 3=C<'cM,#%*3(b<'{8F ??I2EYY :<`1cvbٻc3gq2j[ZI&HnK`2i vVCI{áǟDY0;Odsx"~2L<9c__d.N"I0{A}P|+/Fwc;jTr5B6)!geٻ7^zo Nǂ*;4Qbc- Ɓp\V8d:rGp yqq')*dO[¼d`"fu|gHii)#F|oZpQf2iii%4TBv$ʞXfbP*fxA&ۯ)m?Vrk22Np rQ]TA>A3jMqY1<8E[`tւ뚗ctTf2ۭSel!$P~lZIT#'󑟟OYY&?? p= Xh(lii-ku2K>sw1d4A@6+J؎&[M{:<"'e`ϣasp xz[h RB١ 3wt;!m";Ihk-BD(((b yF 2`ٮ&Tv2Lu}+6M!Jh,P$&ן$(ǥdNt 7WYMQ)wNgo J\aU kfk IM\uscw ''LcLD4JHd2L=7"[6Gۨ #iim V@ 5n!F4dU ;j5>Eh)*($Qg0nk|]M> O:&Jy^D<@D@!a}tЭe5rX,-pǷѮus4L =-Nʜe鲿 ,dc 3 =J/-l+  <.ϣhGvIQЗkW=w=Ё=lˉͭ%mo[gѧwf>鉰-%g'ȧֲ9OwA_NSa@0^]gXz54:`lua ^8c'HS9>lq>.>c?AgljMK[{0m@L_x;jZ9o-_|c~c'Qq?  N.UO |@":Ӧ1k;8'H.;exsf֬@KMٵ;ҭl Zi[6l~u=*[[M V/ڶmg<[o eccw,@AYjlM.1>$;rn}] LK6..1u9fgءj/݀bSk{Ly' l4maɢŬްkQ؂3t( s?ooXΛkHU 2{]wL7 IgxR蓴56|5q@~1:wÑ 姗+Jl.n^yE%rɧ޻N>͏'mmmNv7=7Ira2bhymR6tyُe͖&=7""rՅ/z}-yGZ@Skz>3clc#Ff1Qy#~﹗ffrէ$O2Fc2uHe4zɲY3:;L"`dL9Gɤ+?V?Yԟ!L:ѣ4~~v65+Gfͦzr/W5zoP :\|%~vZWm vP.Ȉ (?߷O=qܜg OSgij-Q!V}'RD*͍95YրRZn_>j| zL8Qm8dp长f#6Ņ('΋^kNcO?ͷrq|SiijD1bcK0sAu$549T:3O` Axay{i#{Emd>aʮm1"҉p묬ٸ޶T׾a0o]L&s XY*p1{ [r6L/Ziu ~[.pTy<Qsq{f;l^^>xRhJ(bѝN^¥ҕBEw[Q;/n5w>; Q QFvA.M硯|?"g5BN9{Tx?xޮ'Ҕ w7` dK}6l^ў^8&F Ȳ7&ܗJyޖg/܅A='t?&h+ҧVMuh25y‘vŕ|4jHtyy&j'$PJ!Zki);7^5bzݍ1xAiXHcQqGAH+V>˞>X1);R=vUBr:[ zOH7]/s4_±;h& OIC&tx+nzޫnȭ[(ʏSɊOl_#hh}6uY:Vաږ{7?D"y~ؓD M 'XtENBhW( _c%cV~ha̝w('e|Y^I#35$Vq Kj{1]D{ОH/#vd P&f,էa&`Yf=￿w]Jͬ_k+Hw*&-Ogæup | Ra#Xj56:=my>to~4O?(?/[higgˤ1cڛkȝO1`D_6PF!LF 1@owv쉑io>.eKmCUڡc2rml͂^amŦ.RRZ]vB6U=E3/ϖ["MǙ>Ibsr3H ȭwIs1>"߷+ƶyk7pՀ}?EeVTBgr*CapfYųMpk1i(*RPGFqY#͓O^klOs)kg߿h~Ǜ9ikm5*3Ng}?"uO~~}\o---H$HxciucZ{︉.WlWhat]ĐH3/^vO@ / "YÒe/(%}^٧NOF \Beú%/#}{@ͺjpFvw(-]0w,!zdir cϾbߺ\|yHeݟp=PU 9 uc]rűm9ɺVixd(6=|q-w\ȧa1@_{%F~: \sկKYy8ҳ6DS[3-&-ߏ~ZB ꩋ*96'n%e4*д6b&moK "ƠʸoFW"]*(y#]0ҎvdVD: hA^S}:jS>8wS8)*o d'[s<Zݬ~KGkLI*9rBOXee%]]&гhXM4ۦu6ou Eqx;(|#|ǔ W0:k-KM.b ;PhoaQQ>ȽH/9gAy//;9G|ousvɠD++\W_|Wz jsڞ8`.<{>^: pYL85㒟ݚuu=ߏ#hones9ihy;GW< /#el\E ~5^M.}??1|粋Rx֚_}]Jpb.3&N U7t[T斌ҌƲ/k3эuqԝ9ɤS4ײzՇԷuD|>:3> S=|Y$V=26md`8߸, h~w{o}?j+>o{t٩{ :A&!~hM#2ݺNI<;E1=T*#"mݽfuؤ"".V]?\7/n ma&Iku|S%6_w$=_%Vbbɉ 4D,dhs;nTx"lR%g.رdnˑ+Wxx'dRRi=]#aKqt"PZʇ?J}/KsRD$)37H/_oɼ⢂{8+ťe,~o476JssK&GL'vCi$cqJ,F󤵖x<.CF$||;uj$IK2f|ߗtz\>P>u=VNErIm{F|/7;Ja`@}߷ZA[s#7Lqbqʇ ec)ʏJ%hkm9j#Rc 2X"ucM&Ά 446TR̐!6{$)DZ|C; ?%T+qI'ill7B~a%%(=r%G) ld2.EEfḁ6 AH"QDh),*0Ǿbxhlj$H!( )-+cH%I,>J;c֖j#u)TDqY9Ç &?$g"vv۾h@|FRix~%ť\M*@Tcn>klGY;{AO;1!i@}D7AL{~p]9;0y[ǂу\̺oHdE5wfuceM2 e5sڱñQM|Gb@C;aXTJ.g0e]2IIL[b~ڪ,+1&Z4ȖkjxЇeb˯dt]Ie|m33&7!h̵@ɒP|}+:U @p=M4훢M7ͦ(vn3ӚkEn7B7~h:QzrTѳG Fs!!{U UL>pyx$eWH-Ecpy`=T5O!7SӰ e_M6l L`G8/,c5Qd4IOxy.XQ T.~U") # YlԂlpYjIXm3wbͼB=J*Pn4i  4A*Tۄ8f`+SbTKel,Md*Tj[*c#k1HVk[ŔhH{wo (i㦈2݉C -R(LFu:K+>O&]Α:+$>}nU8Q#:ºICnLe)Qz?6*6_?"ԃdbLYveU\ZArXSeަM2dK*|dlz qB_iNF{%&_`a<%nUr̾0H ] ?5f2r1tIeMSb?FA۸=fh5rdl?#oW;g 0Ȗr.Be')R@EM\$;rj/Жnzէ7muăHhHdE$WpˑM&PE'c*}?EzLaݜJ2ߡ2AtmuBQфLa˦m9l/b_=ft}hJRA{MWsBÓ jƟ#O53R )wc; ܕ$Tn9qN+azK ri\h [QRxvuNE7Cޫf$jSxs^U-\FաikǦRmJ -@H8QUYD{&͑վgE6;^6劎 Ƥu׋fu tiXC?ɹ[epKyO^HQpQFJcI!)Po7 (f(NHR/O@p9h-/xΛUs[PKeW{6inK)#`ܠS^"(zaFk8n{EDz+y)r=΍qi񓓓 .1C ID-]Wa+ߋ>F]ۯ#t"#f9GMm G1Il5 ~ejlYL**ƕ S1Xܯ-XgPU An H3bY2q"lV{8wN/l7s*Eu% QJ'JBC16yI Qb~I66i;'otWΤ?kΗM)^06*>|"hh0v;A.ا!ZA%.j֛YvJ oڢb5\# d3:d~>&s{-dhV_nKC]L}{^1{t-__GF3*zs^QVisgPda.j tp"l. ރTǴd5L:at;*UzIڿXB͎-$ R,uͲ 1E%.jc}o1(dеo89dC[d&Ncmdg2G?f`PGfؓmxe#:*2xgBs` <(d N5( /`!oQ=6R姰^̑eQѥ)1TXɜqrM῀5)nIw#ecp[諭}dR 96Yzn U !bB툩;GFcJ8H`I#LfM埾KR ܵ0T4inh%#pL$0n1ݺ8i7m髆k:rM}߾R?[A9*.%AJL%_Q6iL?< p+n&X%G]3?G"D*d:kS>7fv&~?q: B x2#i,BʛŞlz;&7|ݜy6NMM*|4q. DUZɑ8 r%/?̓\f' ʪfs7qu=sAy.ו:[36o%j^?XORW4Y qůibbh;>M:?dyYk. 1F*Cj'"n{;&[4:5.T`M\KD" HK/ 7.;*D ڕݟvoIS@eQL B+&o4Y<8k<$NG$'(q rN6V&ݰa7 onƕ WmvK|t!W}Aoyt#iHŁAګhS+&J/!'b >E=bz} 8ִ,t=_o4.(͆2%MLY&[NIfۖYa>g{En]P<7\eaNN,0 1ڿbgWhX>}JsgFv[E[!]%!v[B(dCd>KV*㍐tB՞ۺϾyѼ+~}9*yxLE(5d>.(tr-,j\R8n@u-̒=?քRhz(t:pP˶AԺv 2A݅)-׊/b3 @={~Iμ?fW^NI>Rf|}g/EeݭުںzԑCv݈'sgJ{rGJ %$g?3i^&Ff3'o4(^)6Ҫ4j>Ѵhv!ӹ 1̓^tޑ`]}ŝkS{N@n"h(S&Fk ǖq*BRuGfcE{µ/TT.-}yp}bؠ>y:nSmhLqɢPtyM1D$Q'圤؛ 7[^'vyVY|ʖhRM[&!a/o5ڴ[?:^=JPS9܎<<%4 vÂxlÉ>Ѫ~/b3Kpr:7ewѕX詻))@6D\KW-[F"$@)%AGl&>yG=Kt^),u}F< spHôi]89vnm>x KbΗw?=[Tk6mhlVPG?[ҥiz5I\"]OFr$t]]5Ƹq|¦P_t^CG#349%7^`E̴v?X`_K$7j~ԪZgSkj3uvLڭ!r _ݰ㢣OgSAt Uy[W8%yVA 4,clN<2Mvƹl@!4559O]nC^Z0㡹Bc^۫!VfEcV%!-'Y`< q} kuRB!!`Ш] rXٓuP`-FSOsՄD\Èy:P6k_H9.d@fpWjP-Ѱ_"6ڬ0JV 2ʶRà*Suh**)G0i2itStq;%p<⾄YAQE' =L"8b1Bڶ5MlmRI=uм/\h̗[h5" t%Te03"[]4W#5u v\xɨːPPVqBF|D(\4$ $",O2||8lmi9YwsZ{$4i^~["3]7b0eY:yn=̶0fol?,|9zHWR\D!.^E'Gq-~QJL^d ȃ9g9";TGᠹwyZ0BtUKC ɓ*X j0(Yps !dV:OOdataset-1.6.2/docs/api.rst000066400000000000000000000017601445346461700154220ustar00rootroot00000000000000 API documentation ================= Connecting ---------- .. autofunction:: dataset.connect Notes ----- * **dataset** uses SQLAlchemy connection pooling when connecting to the database. There is no way of explicitly clearing or shutting down the connections, other than having the dataset instance garbage collected. Database -------- .. autoclass:: dataset.Database :members: tables, get_table, create_table, load_table, query, begin, commit, rollback :special-members: Table ----- .. autoclass:: dataset.Table :members: columns, find, find_one, all, count, distinct, insert, insert_ignore, insert_many, update, update_many, upsert, upsert_many, delete, create_column, create_column_by_example, drop_column, create_index, drop, has_column, has_index :special-members: __len__, __iter__ Data Export ----------- **Note:** Data exporting has been extracted into a stand-alone package, datafreeze. See the relevant repository here_. .. _here: https://github.com/pudo/datafreeze dataset-1.6.2/docs/conf.py000066400000000000000000000065151445346461700154210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # dataset documentation build configuration file, created by # sphinx-quickstart on Mon Apr 1 18:41:21 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # 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 # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("../")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = u"dataset" copyright = u"2013-2021, Friedrich Lindenberg, Gregor Aisch, Stefan Wehrmeyer" # 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. version = "1.6.2" # The full version, including alpha/beta/rc tags. release = "1.6.2" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "furo" html_static_path = ["_static"] html_theme_options = { "light_logo": "dataset-logo-light.png", "dark_logo": "dataset-logo-dark.png", } # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # Output file base name for HTML help builder. htmlhelp_basename = "datasetdoc" dataset-1.6.2/docs/index.rst000066400000000000000000000053271445346461700157630ustar00rootroot00000000000000.. dataset documentation master file, created by sphinx-quickstart on Mon Apr 1 18:41:21 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. dataset: databases for lazy people ================================== .. toctree:: :hidden: Although managing data in relational databases has plenty of benefits, they're rarely used in day-to-day work with small to medium scale datasets. But why is that? Why do we see an awful lot of data stored in static files in CSV or JSON format, even though they are hard to query and update incrementally? The answer is that **programmers are lazy**, and thus they tend to prefer the easiest solution they find. And in **Python**, a database isn't the simplest solution for storing a bunch of structured data. This is what **dataset** is going to change! **dataset** provides a simple abstraction layer that removes most direct SQL statements without the necessity for a full ORM model - essentially, databases can be used like a JSON file or NoSQL store. A simple data loading script using **dataset** might look like this: :: import dataset db = dataset.connect('sqlite:///:memory:') table = db['sometable'] table.insert(dict(name='John Doe', age=37)) table.insert(dict(name='Jane Doe', age=34, gender='female')) john = table.find_one(name='John Doe') Here is `similar code, without dataset `_. Features -------- * **Automatic schema**: If a table or column is written that does not exist in the database, it will be created automatically. * **Upserts**: Records are either created or updated, depending on whether an existing version can be found. * **Query helpers** for simple queries such as :py:meth:`all ` rows in a table or all :py:meth:`distinct ` values across a set of columns. * **Compatibility**: Being built on top of `SQLAlchemy `_, ``dataset`` works with all major databases, such as SQLite, PostgreSQL and MySQL. Contents -------- .. toctree:: :maxdepth: 2 install quickstart api queries Contributors ------------ ``dataset`` is written and maintained by `Friedrich Lindenberg `_, `Gregor Aisch `_ and `Stefan Wehrmeyer `_. Its code is largely based on the preceding libraries `sqlaload `_ and datafreeze. And of course, we're standing on the `shoulders of giants `_. Our cute little `naked mole rat `_ was drawn by `Johannes Koch `_. dataset-1.6.2/docs/install.rst000066400000000000000000000011761445346461700163200ustar00rootroot00000000000000 Installation Guide ================== The easiest way is to install ``dataset`` from the `Python Package Index `_ using ``pip`` or ``easy_install``: .. code-block:: bash $ pip install dataset To install it manually simply download the repository from Github: .. code-block:: bash $ git clone git://github.com/pudo/dataset.git $ cd dataset/ $ python setup.py install Depending on the type of database backend, you may also need to install a database specific driver package. For MySQL, this is ``MySQLdb``, for Postgres its ``psycopg2``. SQLite support is integrated into Python. dataset-1.6.2/docs/queries.rst000066400000000000000000000052111445346461700163210ustar00rootroot00000000000000 .. _advanced_filters: Advanced filters ================ ``dataset`` provides two methods for running queries: :py:meth:`table.find() ` and :py:meth:`db.query() `. The table find helper method provides limited, but simple filtering options:: results = table.find(column={operator: value}) # e.g.: results = table.find(name={'like': '%mole rat%'}) A special form is using keyword searches on specific columns:: results = table.find(value=5) # equal to: results = table.find(value={'=': 5}) # Lists, tuples and sets are turned into `IN` queries: results = table.find(category=('foo', 'bar')) # equal to: results = table.find(value={'in': ('foo', 'bar')}) The following comparison operators are supported: ============== ============================================================ Operator Description ============== ============================================================ gt, > Greater than lt, < Less than gte, >= Greater or equal lte, <= Less or equal !=, <>, not Not equal to a single value in Value is in the given sequence notin Value is not in the given sequence like, ilike Text search, ILIKE is case-insensitive. Use ``%`` as a wildcard notlike Like text search, except check if pattern does not exist between, .. Value is between two values in the given tuple startswith String starts with endswith String ends with ============== ============================================================ Querying for a specific value on a column that does not exist on the table will return no results. You can also pass additional SQLAlchemy clauses into the :py:meth:`table.find() ` method by falling back onto the SQLAlchemy core objects wrapped by `dataset`:: # Get the column `city` from the dataset table: column = table.table.columns.city # Define a SQLAlchemy clause: clause = column.ilike('amsterda%') # Query using the clause: results = table.find(clause) This can also be used to define combined OR clauses if needed (e.g. `city = 'Bla' OR country = 'Foo'`). Queries using raw SQL --------------------- To run more complex queries with JOINs, or to perform GROUP BY-style aggregation, you can also use :py:meth:`db.query() ` to run raw SQL queries instead. This also supports parameterisation to avoid SQL injections. Finally, you should consider falling back to SQLAlchemy_ core to construct queries if you are looking for a programmatic, composable method of generating SQL in Python. .. _SQLALchemy: https://docs.sqlalchemy.org/dataset-1.6.2/docs/quickstart.rst000066400000000000000000000163601445346461700170450ustar00rootroot00000000000000 Quickstart ========== Hi, welcome to the twelve-minute quick-start tutorial. Connecting to a database ------------------------ At first you need to import the dataset package :) :: import dataset To connect to a database you need to identify it by its `URL `_, which basically is a string of the form ``"dialect://user:password@host/dbname"``. Here are a few examples for different database backends:: # connecting to a SQLite database db = dataset.connect('sqlite:///mydatabase.db') # connecting to a MySQL database with user and password db = dataset.connect('mysql://user:password@localhost/mydatabase') # connecting to a PostgreSQL database db = dataset.connect('postgresql://scott:tiger@localhost:5432/mydatabase') It is also possible to define the `URL` as an environment variable called `DATABASE_URL` so you can initialize database connection without explicitly passing an `URL`:: db = dataset.connect() Depending on which database you're using, you may also have to install the database bindings to support that database. SQLite is included in the Python core, but PostgreSQL requires ``psycopg2`` to be installed. MySQL can be enabled by installing the ``mysql-db`` drivers. Storing data ------------ To store some data you need to get a reference to a table. You don't need to worry about whether the table already exists or not, since dataset will create it automatically:: # get a reference to the table 'user' table = db['user'] Now storing data in a table is a matter of a single function call. Just pass a `dict`_ to *insert*. Note that you don't need to create the columns *name* and *age* – dataset will do this automatically:: # Insert a new record. table.insert(dict(name='John Doe', age=46, country='China')) # dataset will create "missing" columns any time you insert a dict with an unknown key table.insert(dict(name='Jane Doe', age=37, country='France', gender='female')) .. _dict: http://docs.python.org/2/library/stdtypes.html#dict Updating existing entries is easy, too:: table.update(dict(name='John Doe', age=47), ['name']) The list of filter columns given as the second argument filter using the values in the first column. If you don't want to update over a particular value, just use the auto-generated ``id`` column. Using Transactions ------------------ You can group a set of database updates in a transaction. In that case, all updates are committed at once or, in case of exception, all of them are reverted. Transactions are supported through a context manager, so they can be used through a ``with`` statement:: with dataset.connect() as tx: tx['user'].insert(dict(name='John Doe', age=46, country='China')) You can get same functionality by invoking the methods :py:meth:`begin() `, :py:meth:`commit() ` and :py:meth:`rollback() ` explicitly:: db = dataset.connect() db.begin() try: db['user'].insert(dict(name='John Doe', age=46, country='China')) db.commit() except: db.rollback() Nested transactions are supported too:: db = dataset.connect() with db as tx1: tx1['user'].insert(dict(name='John Doe', age=46, country='China')) with db as tx2: tx2['user'].insert(dict(name='Jane Doe', age=37, country='France', gender='female')) Inspecting databases and tables ------------------------------- When dealing with unknown databases we might want to check their structure first. To start exploring, let's find out what tables are stored in the database: >>> print(db.tables) [u'user'] Now, let's list all columns available in the table ``user``: >>> print(db['user'].columns) [u'id', u'country', u'age', u'name', u'gender'] Using ``len()`` we can get the total number of rows in a table: >>> print(len(db['user'])) 2 Reading data from tables ------------------------ Now let's get some real data out of the table:: users = db['user'].all() If we simply want to iterate over all rows in a table, we can omit :py:meth:`all() `:: for user in db['user']: print(user['age']) We can search for specific entries using :py:meth:`find() ` and :py:meth:`find_one() `:: # All users from China chinese_users = table.find(country='China') # Get a specific user john = table.find_one(name='John Doe') # Find multiple at once winners = table.find(id=[1, 3, 7]) # Find by comparison operator elderly_users = table.find(age={'>=': 70}) possible_customers = table.find(age={'between': [21, 80]}) # Use the underlying SQLAlchemy directly elderly_users = table.find(table.table.columns.age >= 70) See :ref:`advanced_filters` for details on complex filters. Using :py:meth:`distinct() ` we can grab a set of rows with unique values in one or more columns:: # Get one user per country db['user'].distinct('country') Finally, you can use the ``row_type`` parameter to choose the data type in which results will be returned:: import dataset from stuf import stuf db = dataset.connect('sqlite:///mydatabase.db', row_type=stuf) Now contents will be returned in ``stuf`` objects (basically, ``dict`` objects whose elements can be accessed as attributes (``item.name``) as well as by index (``item['name']``). Running custom SQL queries -------------------------- Of course the main reason you're using a database is that you want to use the full power of SQL queries. Here's how you run them with ``dataset``:: result = db.query('SELECT country, COUNT(*) c FROM user GROUP BY country') for row in result: print(row['country'], row['c']) The :py:meth:`query() ` method can also be used to access the underlying `SQLAlchemy core API `_, which allows for the programmatic construction of more complex queries:: table = db['user'].table statement = table.select(table.c.name.like('%John%')) result = db.query(statement) Limitations of dataset ---------------------- The goal of ``dataset`` is to make basic database operations simpler, by expressing some relatively basic operations in a Pythonic way. The downside of this approach is that as your application grows more complex, you may begin to need access to more advanced operations and be forced to switch to using SQLAlchemy proper, without the dataset layer (instead, you may want to play with SQLAlchemy's ORM). When that moment comes, take the hit. SQLAlchemy is an amazing piece of Python code, and it will provide you with idiomatic access to all of SQL's functions. Some of the specific aspects of SQL that are not exposed in ``dataset``, and are considered out of scope for the project, include: * Foreign key relationships between tables, and expressing one-to-many and many-to-many relationships in idiomatic Python. * Python-wrapped ``JOIN`` queries. * Creating databases, or managing DBMS software. * Support for Python 2.x There's also some functionality that might be cool to support in the future, but that requires significant engineering: * Async operations * Database-native ``UPSERT`` semantics dataset-1.6.2/docs/requirements.txt000066400000000000000000000000041445346461700173710ustar00rootroot00000000000000furodataset-1.6.2/setup.cfg000066400000000000000000000002001445346461700147740ustar00rootroot00000000000000[metadata] description-file = README.md [flake8] ignore = E501,E123,E124,E126,E127,E128,E722,E741 [bdist_wheel] universal = 1 dataset-1.6.2/setup.py000066400000000000000000000030561445346461700147010ustar00rootroot00000000000000from setuptools import setup, find_packages with open("README.md") as f: long_description = f.read() setup( name="dataset", version="1.6.2", description="Toolkit for Python-based database access.", long_description=long_description, long_description_content_type="text/markdown", classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", ], keywords="sql sqlalchemy etl loading utility", author="Friedrich Lindenberg, Gregor Aisch, Stefan Wehrmeyer", author_email="friedrich.lindenberg@gmail.com", url="http://github.com/pudo/dataset", license="MIT", packages=find_packages(exclude=["ez_setup", "examples", "test"]), namespace_packages=[], include_package_data=False, zip_safe=False, install_requires=[ "sqlalchemy >= 1.3.2, < 2.0.0", "alembic >= 0.6.2", "banal >= 1.0.1", ], extras_require={ "dev": [ "pip", "pytest", "wheel", "flake8", "coverage", "psycopg2-binary", "PyMySQL", "cryptography", ] }, tests_require=["pytest"], test_suite="test", entry_points={}, ) dataset-1.6.2/test/000077500000000000000000000000001445346461700141425ustar00rootroot00000000000000dataset-1.6.2/test/__init__.py000066400000000000000000000000001445346461700162410ustar00rootroot00000000000000dataset-1.6.2/test/sample_data.py000066400000000000000000000011611445346461700167650ustar00rootroot00000000000000# -*- encoding: utf-8 -*- from __future__ import unicode_literals from datetime import datetime TEST_CITY_1 = "B€rkeley" TEST_CITY_2 = "G€lway" TEST_DATA = [ {"date": datetime(2011, 1, 1), "temperature": 1, "place": TEST_CITY_2}, {"date": datetime(2011, 1, 2), "temperature": -1, "place": TEST_CITY_2}, {"date": datetime(2011, 1, 3), "temperature": 0, "place": TEST_CITY_2}, {"date": datetime(2011, 1, 1), "temperature": 6, "place": TEST_CITY_1}, {"date": datetime(2011, 1, 2), "temperature": 8, "place": TEST_CITY_1}, {"date": datetime(2011, 1, 3), "temperature": 5, "place": TEST_CITY_1}, ] dataset-1.6.2/test/test_dataset.py000066400000000000000000000526241445346461700172110ustar00rootroot00000000000000import os import unittest from datetime import datetime from collections import OrderedDict from sqlalchemy import TEXT, BIGINT from sqlalchemy.exc import IntegrityError, SQLAlchemyError, ArgumentError from dataset import connect, chunked from .sample_data import TEST_DATA, TEST_CITY_1 class DatabaseTestCase(unittest.TestCase): def setUp(self): self.db = connect() self.tbl = self.db["weather"] self.tbl.insert_many(TEST_DATA) def tearDown(self): for table in self.db.tables: self.db[table].drop() def test_valid_database_url(self): assert self.db.url, os.environ["DATABASE_URL"] def test_database_url_query_string(self): db = connect("sqlite:///:memory:/?cached_statements=1") assert "cached_statements" in db.url, db.url def test_tables(self): assert self.db.tables == ["weather"], self.db.tables def test_contains(self): assert "weather" in self.db, self.db.tables def test_create_table(self): table = self.db["foo"] assert self.db.has_table(table.table.name) assert len(table.table.columns) == 1, table.table.columns assert "id" in table.table.c, table.table.c def test_create_table_no_ids(self): if "mysql" in self.db.engine.dialect.dbapi.__name__: return if "sqlite" in self.db.engine.dialect.dbapi.__name__: return table = self.db.create_table("foo_no_id", primary_id=False) assert table.table.exists() assert len(table.table.columns) == 0, table.table.columns def test_create_table_custom_id1(self): pid = "string_id" table = self.db.create_table("foo2", pid, self.db.types.string(255)) assert self.db.has_table(table.table.name) assert len(table.table.columns) == 1, table.table.columns assert pid in table.table.c, table.table.c table.insert({pid: "foobar"}) assert table.find_one(string_id="foobar")[pid] == "foobar" def test_create_table_custom_id2(self): pid = "string_id" table = self.db.create_table("foo3", pid, self.db.types.string(50)) assert self.db.has_table(table.table.name) assert len(table.table.columns) == 1, table.table.columns assert pid in table.table.c, table.table.c table.insert({pid: "foobar"}) assert table.find_one(string_id="foobar")[pid] == "foobar" def test_create_table_custom_id3(self): pid = "int_id" table = self.db.create_table("foo4", primary_id=pid) assert self.db.has_table(table.table.name) assert len(table.table.columns) == 1, table.table.columns assert pid in table.table.c, table.table.c table.insert({pid: 123}) table.insert({pid: 124}) assert table.find_one(int_id=123)[pid] == 123 assert table.find_one(int_id=124)[pid] == 124 self.assertRaises(IntegrityError, lambda: table.insert({pid: 123})) def test_create_table_shorthand1(self): pid = "int_id" table = self.db.get_table("foo5", pid) assert table.table.exists assert len(table.table.columns) == 1, table.table.columns assert pid in table.table.c, table.table.c table.insert({"int_id": 123}) table.insert({"int_id": 124}) assert table.find_one(int_id=123)["int_id"] == 123 assert table.find_one(int_id=124)["int_id"] == 124 self.assertRaises(IntegrityError, lambda: table.insert({"int_id": 123})) def test_create_table_shorthand2(self): pid = "string_id" table = self.db.get_table( "foo6", primary_id=pid, primary_type=self.db.types.string(255) ) assert table.table.exists assert len(table.table.columns) == 1, table.table.columns assert pid in table.table.c, table.table.c table.insert({"string_id": "foobar"}) assert table.find_one(string_id="foobar")["string_id"] == "foobar" def test_with(self): init_length = len(self.db["weather"]) with self.assertRaises(ValueError): with self.db as tx: tx["weather"].insert( { "date": datetime(2011, 1, 1), "temperature": 1, "place": "tmp_place", } ) raise ValueError() assert len(self.db["weather"]) == init_length def test_invalid_values(self): if "mysql" in self.db.engine.dialect.dbapi.__name__: # WARNING: mysql seems to be doing some weird type casting # upon insert. The mysql-python driver is not affected but # it isn't compatible with Python 3 # Conclusion: use postgresql. return with self.assertRaises(SQLAlchemyError): tbl = self.db["weather"] tbl.insert( {"date": True, "temperature": "wrong_value", "place": "tmp_place"} ) def test_load_table(self): tbl = self.db.load_table("weather") assert tbl.table.name == self.tbl.table.name def test_query(self): r = self.db.query("SELECT COUNT(*) AS num FROM weather").next() assert r["num"] == len(TEST_DATA), r def test_table_cache_updates(self): tbl1 = self.db.get_table("people") data = OrderedDict([("first_name", "John"), ("last_name", "Smith")]) tbl1.insert(data) data["id"] = 1 tbl2 = self.db.get_table("people") assert dict(tbl2.all().next()) == dict(data), (tbl2.all().next(), data) class TableTestCase(unittest.TestCase): def setUp(self): self.db = connect() self.tbl = self.db["weather"] for row in TEST_DATA: self.tbl.insert(row) def tearDown(self): self.tbl.drop() def test_insert(self): assert len(self.tbl) == len(TEST_DATA), len(self.tbl) last_id = self.tbl.insert( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"} ) assert len(self.tbl) == len(TEST_DATA) + 1, len(self.tbl) assert self.tbl.find_one(id=last_id)["place"] == "Berlin" def test_insert_ignore(self): self.tbl.insert_ignore( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"}, ["place"], ) assert len(self.tbl) == len(TEST_DATA) + 1, len(self.tbl) self.tbl.insert_ignore( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"}, ["place"], ) assert len(self.tbl) == len(TEST_DATA) + 1, len(self.tbl) def test_insert_ignore_all_key(self): for i in range(0, 4): self.tbl.insert_ignore( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"}, ["date", "temperature", "place"], ) assert len(self.tbl) == len(TEST_DATA) + 1, len(self.tbl) def test_insert_json(self): last_id = self.tbl.insert( { "date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin", "info": { "currency": "EUR", "language": "German", "population": 3292365, }, } ) assert len(self.tbl) == len(TEST_DATA) + 1, len(self.tbl) assert self.tbl.find_one(id=last_id)["place"] == "Berlin" def test_upsert(self): self.tbl.upsert( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"}, ["place"], ) assert len(self.tbl) == len(TEST_DATA) + 1, len(self.tbl) self.tbl.upsert( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"}, ["place"], ) assert len(self.tbl) == len(TEST_DATA) + 1, len(self.tbl) def test_upsert_single_column(self): table = self.db["banana_single_col"] table.upsert({"color": "Yellow"}, ["color"]) assert len(table) == 1, len(table) table.upsert({"color": "Yellow"}, ["color"]) assert len(table) == 1, len(table) def test_upsert_all_key(self): assert len(self.tbl) == len(TEST_DATA), len(self.tbl) for i in range(0, 2): self.tbl.upsert( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"}, ["date", "temperature", "place"], ) assert len(self.tbl) == len(TEST_DATA) + 1, len(self.tbl) def test_upsert_id(self): table = self.db["banana_with_id"] data = dict(id=10, title="I am a banana!") table.upsert(data, ["id"]) assert len(table) == 1, len(table) def test_update_while_iter(self): for row in self.tbl: row["foo"] = "bar" self.tbl.update(row, ["place", "date"]) assert len(self.tbl) == len(TEST_DATA), len(self.tbl) def test_weird_column_names(self): with self.assertRaises(ValueError): self.tbl.insert( { "date": datetime(2011, 1, 2), "temperature": -10, "foo.bar": "Berlin", "qux.bar": "Huhu", } ) def test_cased_column_names(self): tbl = self.db["cased_column_names"] tbl.insert({"place": "Berlin"}) tbl.insert({"Place": "Berlin"}) tbl.insert({"PLACE ": "Berlin"}) assert len(tbl.columns) == 2, tbl.columns assert len(list(tbl.find(Place="Berlin"))) == 3 assert len(list(tbl.find(place="Berlin"))) == 3 assert len(list(tbl.find(PLACE="Berlin"))) == 3 def test_invalid_column_names(self): tbl = self.db["weather"] with self.assertRaises(ValueError): tbl.insert({None: "banana"}) with self.assertRaises(ValueError): tbl.insert({"": "banana"}) with self.assertRaises(ValueError): tbl.insert({"-": "banana"}) def test_delete(self): self.tbl.insert( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"} ) original_count = len(self.tbl) assert len(self.tbl) == len(TEST_DATA) + 1, len(self.tbl) # Test bad use of API with self.assertRaises(ArgumentError): self.tbl.delete({"place": "Berlin"}) assert len(self.tbl) == original_count, len(self.tbl) assert self.tbl.delete(place="Berlin") is True, "should return 1" assert len(self.tbl) == len(TEST_DATA), len(self.tbl) assert self.tbl.delete() is True, "should return non zero" assert len(self.tbl) == 0, len(self.tbl) def test_repr(self): assert ( repr(self.tbl) == "" ), "the representation should be " def test_delete_nonexist_entry(self): assert ( self.tbl.delete(place="Berlin") is False ), "entry not exist, should fail to delete" def test_find_one(self): self.tbl.insert( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"} ) d = self.tbl.find_one(place="Berlin") assert d["temperature"] == -10, d d = self.tbl.find_one(place="Atlantis") assert d is None, d def test_count(self): assert len(self.tbl) == 6, len(self.tbl) length = self.tbl.count(place=TEST_CITY_1) assert length == 3, length def test_find(self): ds = list(self.tbl.find(place=TEST_CITY_1)) assert len(ds) == 3, ds ds = list(self.tbl.find(place=TEST_CITY_1, _limit=2)) assert len(ds) == 2, ds ds = list(self.tbl.find(place=TEST_CITY_1, _limit=2, _step=1)) assert len(ds) == 2, ds ds = list(self.tbl.find(place=TEST_CITY_1, _limit=1, _step=2)) assert len(ds) == 1, ds ds = list(self.tbl.find(_step=2)) assert len(ds) == len(TEST_DATA), ds ds = list(self.tbl.find(order_by=["temperature"])) assert ds[0]["temperature"] == -1, ds ds = list(self.tbl.find(order_by=["-temperature"])) assert ds[0]["temperature"] == 8, ds ds = list(self.tbl.find(self.tbl.table.columns.temperature > 4)) assert len(ds) == 3, ds def test_find_dsl(self): ds = list(self.tbl.find(place={"like": "%lw%"})) assert len(ds) == 3, ds ds = list(self.tbl.find(temperature={">": 5})) assert len(ds) == 2, ds ds = list(self.tbl.find(temperature={">=": 5})) assert len(ds) == 3, ds ds = list(self.tbl.find(temperature={"<": 0})) assert len(ds) == 1, ds ds = list(self.tbl.find(temperature={"<=": 0})) assert len(ds) == 2, ds ds = list(self.tbl.find(temperature={"!=": -1})) assert len(ds) == 5, ds ds = list(self.tbl.find(temperature={"between": [5, 8]})) assert len(ds) == 3, ds ds = list(self.tbl.find(place={"=": "G€lway"})) assert len(ds) == 3, ds ds = list(self.tbl.find(place={"ilike": "%LwAy"})) assert len(ds) == 3, ds def test_offset(self): ds = list(self.tbl.find(place=TEST_CITY_1, _offset=1)) assert len(ds) == 2, ds ds = list(self.tbl.find(place=TEST_CITY_1, _limit=2, _offset=2)) assert len(ds) == 1, ds def test_streamed(self): ds = list(self.tbl.find(place=TEST_CITY_1, _streamed=True, _step=1)) assert len(ds) == 3, len(ds) for row in self.tbl.find(place=TEST_CITY_1, _streamed=True, _step=1): row["temperature"] = -1 self.tbl.update(row, ["id"]) def test_distinct(self): x = list(self.tbl.distinct("place")) assert len(x) == 2, x x = list(self.tbl.distinct("place", "date")) assert len(x) == 6, x x = list( self.tbl.distinct( "place", "date", self.tbl.table.columns.date >= datetime(2011, 1, 2, 0, 0), ) ) assert len(x) == 4, x x = list(self.tbl.distinct("temperature", place="B€rkeley")) assert len(x) == 3, x x = list(self.tbl.distinct("temperature", place=["B€rkeley", "G€lway"])) assert len(x) == 6, x def test_insert_many(self): data = TEST_DATA * 100 self.tbl.insert_many(data, chunk_size=13) assert len(self.tbl) == len(data) + 6, (len(self.tbl), len(data)) def test_chunked_insert(self): data = TEST_DATA * 100 with chunked.ChunkedInsert(self.tbl) as chunk_tbl: for item in data: chunk_tbl.insert(item) assert len(self.tbl) == len(data) + 6, (len(self.tbl), len(data)) def test_chunked_insert_callback(self): data = TEST_DATA * 100 N = 0 def callback(queue): nonlocal N N += len(queue) with chunked.ChunkedInsert(self.tbl, callback=callback) as chunk_tbl: for item in data: chunk_tbl.insert(item) assert len(data) == N assert len(self.tbl) == len(data) + 6 def test_update_many(self): tbl = self.db["update_many_test"] tbl.insert_many([dict(temp=10), dict(temp=20), dict(temp=30)]) tbl.update_many([dict(id=1, temp=50), dict(id=3, temp=50)], "id") # Ensure data has been updated. assert tbl.find_one(id=1)["temp"] == tbl.find_one(id=3)["temp"] def test_chunked_update(self): tbl = self.db["update_many_test"] tbl.insert_many( [ dict(temp=10, location="asdf"), dict(temp=20, location="qwer"), dict(temp=30, location="asdf"), ] ) chunked_tbl = chunked.ChunkedUpdate(tbl, "id") chunked_tbl.update(dict(id=1, temp=50)) chunked_tbl.update(dict(id=2, location="asdf")) chunked_tbl.update(dict(id=3, temp=50)) chunked_tbl.flush() # Ensure data has been updated. assert tbl.find_one(id=1)["temp"] == tbl.find_one(id=3)["temp"] == 50 assert ( tbl.find_one(id=2)["location"] == tbl.find_one(id=3)["location"] == "asdf" ) # noqa def test_upsert_many(self): # Also tests updating on records with different attributes tbl = self.db["upsert_many_test"] W = 100 tbl.upsert_many([dict(age=10), dict(weight=W)], "id") assert tbl.find_one(id=1)["age"] == 10 tbl.upsert_many([dict(id=1, age=70), dict(id=2, weight=W / 2)], "id") assert tbl.find_one(id=2)["weight"] == W / 2 def test_drop_operations(self): assert self.tbl._table is not None, "table shouldn't be dropped yet" self.tbl.drop() assert self.tbl._table is None, "table should be dropped now" assert list(self.tbl.all()) == [], self.tbl.all() assert self.tbl.count() == 0, self.tbl.count() def test_table_drop(self): assert "weather" in self.db self.db["weather"].drop() assert "weather" not in self.db def test_table_drop_then_create(self): assert "weather" in self.db self.db["weather"].drop() assert "weather" not in self.db self.db["weather"].insert({"foo": "bar"}) def test_columns(self): cols = self.tbl.columns assert len(list(cols)) == 4, "column count mismatch" assert "date" in cols and "temperature" in cols and "place" in cols def test_drop_column(self): try: self.tbl.drop_column("date") assert "date" not in self.tbl.columns except RuntimeError: pass def test_iter(self): c = 0 for row in self.tbl: c += 1 assert c == len(self.tbl) def test_update(self): date = datetime(2011, 1, 2) res = self.tbl.update( {"date": date, "temperature": -10, "place": TEST_CITY_1}, ["place", "date"] ) assert res, "update should return True" m = self.tbl.find_one(place=TEST_CITY_1, date=date) assert m["temperature"] == -10, ( "new temp. should be -10 but is %d" % m["temperature"] ) def test_create_column(self): tbl = self.tbl flt = self.db.types.float tbl.create_column("foo", flt) assert "foo" in tbl.table.c, tbl.table.c assert isinstance(tbl.table.c["foo"].type, flt), tbl.table.c["foo"].type assert "foo" in tbl.columns, tbl.columns def test_ensure_column(self): tbl = self.tbl flt = self.db.types.float tbl.create_column_by_example("foo", 0.1) assert "foo" in tbl.table.c, tbl.table.c assert isinstance(tbl.table.c["foo"].type, flt), tbl.table.c["bar"].type tbl.create_column_by_example("bar", 1) assert "bar" in tbl.table.c, tbl.table.c assert isinstance(tbl.table.c["bar"].type, BIGINT), tbl.table.c["bar"].type tbl.create_column_by_example("pippo", "test") assert "pippo" in tbl.table.c, tbl.table.c assert isinstance(tbl.table.c["pippo"].type, TEXT), tbl.table.c["pippo"].type tbl.create_column_by_example("bigbar", 11111111111) assert "bigbar" in tbl.table.c, tbl.table.c assert isinstance(tbl.table.c["bigbar"].type, BIGINT), tbl.table.c[ "bigbar" ].type tbl.create_column_by_example("littlebar", -11111111111) assert "littlebar" in tbl.table.c, tbl.table.c assert isinstance(tbl.table.c["littlebar"].type, BIGINT), tbl.table.c[ "littlebar" ].type def test_key_order(self): res = self.db.query("SELECT temperature, place FROM weather LIMIT 1") keys = list(res.next().keys()) assert keys[0] == "temperature" assert keys[1] == "place" def test_empty_query(self): empty = list(self.tbl.find(place="not in data")) assert len(empty) == 0, empty class Constructor(dict): """Very simple low-functionality extension to ``dict`` to provide attribute access to dictionary contents""" def __getattr__(self, name): return self[name] class RowTypeTestCase(unittest.TestCase): def setUp(self): self.db = connect(row_type=Constructor) self.tbl = self.db["weather"] for row in TEST_DATA: self.tbl.insert(row) def tearDown(self): for table in self.db.tables: self.db[table].drop() def test_find_one(self): self.tbl.insert( {"date": datetime(2011, 1, 2), "temperature": -10, "place": "Berlin"} ) d = self.tbl.find_one(place="Berlin") assert d["temperature"] == -10, d assert d.temperature == -10, d d = self.tbl.find_one(place="Atlantis") assert d is None, d def test_find(self): ds = list(self.tbl.find(place=TEST_CITY_1)) assert len(ds) == 3, ds for item in ds: assert isinstance(item, Constructor), item ds = list(self.tbl.find(place=TEST_CITY_1, _limit=2)) assert len(ds) == 2, ds for item in ds: assert isinstance(item, Constructor), item def test_distinct(self): x = list(self.tbl.distinct("place")) assert len(x) == 2, x for item in x: assert isinstance(item, Constructor), item x = list(self.tbl.distinct("place", "date")) assert len(x) == 6, x for item in x: assert isinstance(item, Constructor), item def test_iter(self): c = 0 for row in self.tbl: c += 1 assert isinstance(row, Constructor), row assert c == len(self.tbl) if __name__ == "__main__": unittest.main()