diff --git a/CHANGES.rst b/CHANGES.rst index de8c8f1..30e1e3b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,14 @@ Changes ======= +Version 1.0.1 (2022-12-06) +-------------------------- + +- Remove integration with lccs-db `#187 `_. +- Improve performance on items retrieval +- Fix migration 0.8 - 1.0 related bands metadata ``null`` entry `#189 `_. + + Version 1.0.0 (2022-09-22) -------------------------- diff --git a/MANIFEST.in b/MANIFEST.in index 497d1ad..0984841 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -38,3 +38,4 @@ recursive-include docs *.rst recursive-include docs Makefile recursive-include examples *.py recursive-include tests *.py +recursive-include examples *.json diff --git a/USAGE.rst b/USAGE.rst index 86d1f41..fde2168 100644 --- a/USAGE.rst +++ b/USAGE.rst @@ -41,7 +41,6 @@ all definitions for ``BDC-Catalog``:: bdc-db db init bdc-db db create-namespaces bdc-db db create-extension-postgis - lccs-db db create-extension-hstore bdc-db db create-schema @@ -62,9 +61,6 @@ Creating database definition SQLALCHEMY_DATABASE_URI="postgresql://postgres:postgres@localhost:5432/bdcdb" \ bdc-db db create-extension-postgis - SQLALCHEMY_DATABASE_URI="postgresql://postgres:postgres@localhost:5432/bdcdb" \ - lccs-db db create-extension-hstore - **2.** After that, run ``BDC-DB`` command to prepare the Brazil Data Cube data model:: diff --git a/bdc_catalog/alembic/5067fb4381c0_add_roles_system.py b/bdc_catalog/alembic/5067fb4381c0_add_roles_system.py index df598a4..5ddfb35 100644 --- a/bdc_catalog/alembic/5067fb4381c0_add_roles_system.py +++ b/bdc_catalog/alembic/5067fb4381c0_add_roles_system.py @@ -44,10 +44,6 @@ def upgrade(): schema='bdc' ) - op.drop_constraint('collections_classification_system_id_class_systems_fkey', 'collections', schema='bdc', type_='foreignkey') - op.create_foreign_key(op.f('collections_classification_system_id_classification_systems_fkey'), 'collections', 'classification_systems', ['classification_system_id'], ['id'], source_schema='bdc', referent_schema='lccs', onupdate='CASCADE', ondelete='CASCADE') - op.drop_column('collections', 'is_public', schema='bdc') - op.create_index(op.f('idx_bdc_quicklook_collection_id'), 'quicklook', ['collection_id'], unique=False, schema='bdc') # ### end Alembic commands ### @@ -56,9 +52,6 @@ def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f('idx_bdc_quicklook_collection_id'), table_name='quicklook', schema='bdc') - op.drop_constraint(op.f('collections_classification_system_id_classification_systems_fkey'), 'collections', schema='bdc', type_='foreignkey') - op.create_foreign_key('collections_classification_system_id_class_systems_fkey', 'collections', 'classification_systems', ['classification_system_id'], ['id'], source_schema='bdc', referent_schema='lccs') - op.drop_table('collections_roles', schema='bdc') op.drop_index(op.f('idx_bdc_roles_name'), table_name='roles', schema='bdc') op.drop_table('roles', schema='bdc') diff --git a/bdc_catalog/alembic/c68b17b1860b_v0_8_0.py b/bdc_catalog/alembic/c68b17b1860b_v0_8_0.py index 7d043b0..877e4fc 100644 --- a/bdc_catalog/alembic/c68b17b1860b_v0_8_0.py +++ b/bdc_catalog/alembic/c68b17b1860b_v0_8_0.py @@ -12,21 +12,14 @@ revision = 'c68b17b1860b' down_revision = '566a05da999d' branch_labels = () -depends_on = 'e8b12ba52665' # LCCS-DB reference id +# Do not remove this migration. +# This part is important to keep migration tree + def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('collections', sa.Column('classification_system_id', sa.Integer(), nullable=True), schema='bdc') - op.create_foreign_key(op.f('collections_classification_system_id_class_systems_fkey'), 'collections', 'classification_systems', ['classification_system_id'], ['id'], source_schema='bdc', referent_schema='lccs', onupdate='CASCADE', ondelete='CASCADE') - op.create_index(op.f('idx_bdc_collections_classification_system_id'), 'collections', ['classification_system_id'], - unique=False, schema='bdc') - # ### end Alembic commands ### + pass def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('idx_bdc_collections_classification_system_id'), table_name='collections', schema='bdc') - op.drop_constraint(op.f('collections_classification_system_id_classification_systems_fkey'), 'collections', schema='bdc', type_='foreignkey') - op.drop_column('collections', 'classification_system_id', schema='bdc') - # ### end Alembic commands ### + pass diff --git a/bdc_catalog/alembic/d01f09b5dd8b_v1_0_0.py b/bdc_catalog/alembic/d01f09b5dd8b_v1_0_0.py index b5ff742..3a0a2ee 100644 --- a/bdc_catalog/alembic/d01f09b5dd8b_v1_0_0.py +++ b/bdc_catalog/alembic/d01f09b5dd8b_v1_0_0.py @@ -17,7 +17,6 @@ revision = 'd01f09b5dd8b' down_revision = 'c68b17b1860b' branch_labels = () -depends_on = '561ebe6266ad' # LCCS-DB stable 0.8.1 def upgrade(): @@ -56,7 +55,7 @@ def upgrade(): comment='The value to sum in scale mult')) sql = """UPDATE bdc.bands - SET metadata = coalesce(metadata::jsonb,'{}'::jsonb) || ('{\"eo\":{\"resolution_x\":'||resolution_x||',\"resolution_y\":'||resolution_y||',\"center_wavelength\":'||coalesce(center_wavelength, 0)||',\"full_width_half_max\": '||coalesce(full_width_half_max, 0)||'}}')::jsonb""" + SET metadata = CASE WHEN metadata::TEXT = 'null' THEN '{}'::jsonb ELSE coalesce(metadata::jsonb,'{}'::jsonb) || ('{\"eo\":{\"resolution_x\":'||resolution_x||',\"resolution_y\":'||resolution_y||',\"center_wavelength\":'||coalesce(center_wavelength, 0)||',\"full_width_half_max\": '||coalesce(full_width_half_max, 0)||'}}')::jsonb END""" bands_op.execute(sql) bands_op.drop_column('center_wavelength') bands_op.drop_column('full_width_half_max') diff --git a/bdc_catalog/alembic/f3112636be24_remove_lccs_db.py b/bdc_catalog/alembic/f3112636be24_remove_lccs_db.py new file mode 100644 index 0000000..75ac967 --- /dev/null +++ b/bdc_catalog/alembic/f3112636be24_remove_lccs_db.py @@ -0,0 +1,79 @@ +"""remove lccs-db + +Revision ID: f3112636be24 +Revises: 561ebe6266ad +Create Date: 2022-11-01 17:05:17.726004 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +from sqlalchemy.engine.reflection import Inspector + +# revision identifiers, used by Alembic. +revision = 'f3112636be24' +down_revision = '5067fb4381c0' +branch_labels = () +depends_on = None + + +def _has_column(table_name: str, column: str, schema: str = None) -> bool: + bind = op.get_bind() + inspector = Inspector.from_engine(bind=bind) + + found_column = False + for col in inspector.get_columns(table_name=table_name, schema=schema): + if column in col['name']: + found_column = True + break + + return found_column + + +def upgrade(): + with op.batch_alter_table('collections', schema='bdc') as batch_op: + if _has_column(table_name='collections', schema='bdc', column='classification_system_id'): + batch_op.drop_column('classification_system_id') + if not _has_column(table_name='collections', schema='bdc', column='is_public'): + batch_op.add_column(sa.Column('is_public', sa.Boolean(), nullable=False, server_default='true')) + batch_op.create_index(op.f('idx_bdc_collections_is_public'), ['is_public'], unique=False) + + op.create_index('idx_bdc_items_start_date_desc_id', 'items', [sa.text('start_date DESC'), 'id'], unique=False, + schema='bdc') + op.create_index('idx_bdc_items_start_date_desc_id_is_available', 'items', + [sa.text('start_date DESC'), 'id', 'is_available'], unique=False, + schema='bdc') + op.drop_table('collections_roles', schema='bdc') + op.drop_table('roles', schema='bdc') + # ### end Alembic commands ### + + +def downgrade(): + op.create_table( + 'roles', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('roles_pkey')), + sa.UniqueConstraint('name', name=op.f('roles_name_key')), + schema='bdc' + ) + op.create_index(op.f('idx_bdc_roles_name'), 'roles', ['name'], unique=False, schema='bdc') + op.create_table( + 'collections_roles', + sa.Column('collection_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['collection_id'], ['bdc.collections.id'], + name=op.f('collections_roles_collection_id_collections_fkey'), onupdate='CASCADE', + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['bdc.roles.id'], name=op.f('collections_roles_role_id_roles_fkey'), + onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('collection_id', 'role_id', name=op.f('collections_roles_pkey')), + schema='bdc' + ) + op.drop_index('idx_bdc_items_start_date_desc_id', table_name='items', schema='bdc') + op.drop_index('idx_bdc_items_start_date_desc_id_is_available', table_name='items', schema='bdc') diff --git a/bdc_catalog/cli.py b/bdc_catalog/cli.py index 4882fb0..38e588a 100644 --- a/bdc_catalog/cli.py +++ b/bdc_catalog/cli.py @@ -72,14 +72,16 @@ def load_data(ifile: str, from_dir: str, verbose: bool): 'Make sure to install with "pip install shapely" or ' '"pip install -e .[geo]".') + if not ifile and not from_dir: + raise click.MissingParameter("Missing --ifile or --from-dir parameter.") + entries = [] + if ifile: entries.append(Path(ifile)) elif from_dir: for entry in Path(from_dir).glob('*.json'): entries.append(entry) - else: - raise click.MissingParameter("Missing --ifile or --from-dir parameter.") for entry in entries: with entry.open() as fd: diff --git a/bdc_catalog/ext.py b/bdc_catalog/ext.py index 74a468a..676e4ef 100644 --- a/bdc_catalog/ext.py +++ b/bdc_catalog/ext.py @@ -83,7 +83,7 @@ def __init__(self, app=None): if app: self.init_app(app) - def init_app(self, app: Flask): + def init_app(self, app: Flask, **kwargs): """Initialize Flask application instance. Args: diff --git a/bdc_catalog/models/__init__.py b/bdc_catalog/models/__init__.py index 9ed0d26..8db22a5 100644 --- a/bdc_catalog/models/__init__.py +++ b/bdc_catalog/models/__init__.py @@ -20,8 +20,7 @@ from .band import Band, BandSRC from .base_sql import db -from .collection import (Collection, CollectionRole, CollectionsProviders, - CollectionSRC) +from .collection import Collection, CollectionsProviders, CollectionSRC from .composite_function import CompositeFunction from .grid_ref_sys import GridRefSys from .item import Item, ItemsProcessors, SpatialRefSys @@ -30,7 +29,6 @@ from .provider import Provider from .quicklook import Quicklook from .resolution_unit import ResolutionUnit -from .role import Role from .tile import Tile from .timeline import Timeline @@ -39,7 +37,6 @@ 'Band', 'BandSRC', 'Collection', - 'CollectionRole', 'CollectionSRC', 'CollectionsProviders', 'CompositeFunction', @@ -51,7 +48,6 @@ 'Provider', 'Quicklook', 'ResolutionUnit', - 'Role', 'SpatialRefSys', 'Tile', 'Timeline', diff --git a/bdc_catalog/models/collection.py b/bdc_catalog/models/collection.py index 0b46d8e..5f678e0 100644 --- a/bdc_catalog/models/collection.py +++ b/bdc_catalog/models/collection.py @@ -22,7 +22,6 @@ from bdc_db.sqltypes import JSONB from geoalchemy2 import Geometry -from lccs_db.models import LucClassificationSystem from sqlalchemy import (ARRAY, TIMESTAMP, Boolean, Column, Enum, ForeignKey, Index, Integer, PrimaryKeyConstraint, String, Text, UniqueConstraint) @@ -33,7 +32,6 @@ from ..config import BDC_CATALOG_SCHEMA from .base_sql import BaseModel, db from .provider import Provider -from .role import Role name_collection_type = 'collection_type' options_collection_type = ('cube', 'collection', 'classification', 'mosaic') @@ -57,7 +55,6 @@ class Collection(BaseModel): ForeignKey(f'{BDC_CATALOG_SCHEMA}.composite_functions.id', onupdate='CASCADE', ondelete='CASCADE'), comment='Function schema identifier. Used for data cubes.') grid_ref_sys_id = Column(ForeignKey(f'{BDC_CATALOG_SCHEMA}.grid_ref_sys.id', onupdate='CASCADE', ondelete='CASCADE')) - classification_system_id = Column(ForeignKey(LucClassificationSystem.id, onupdate='CASCADE', ondelete='CASCADE')) collection_type = Column(enum_collection_type, nullable=False) metadata_ = Column('metadata', JSONB('bdc-catalog/collection-metadata.json'), comment='Follow the JSONSchema @jsonschemas/collection-metadata.json') @@ -69,6 +66,7 @@ class Collection(BaseModel): item_assets = Column('item_assets', JSONB('bdc-catalog/collection-item-assets.json'), comment='Contains the STAC Extension Item Assets.') is_available = Column(Boolean(), nullable=False, default=False, server_default='False') + is_public = Column(Boolean(), nullable=False, default=True, server_default='true') category = Column(enum_collection_category, nullable=False) start_date = Column(TIMESTAMP(timezone=True)) end_date = Column(TIMESTAMP(timezone=True)) @@ -82,17 +80,15 @@ class Collection(BaseModel): bands = relationship('Band', back_populates='collection') quicklook = relationship('Quicklook') timeline = relationship('Timeline') - # Joined Eager Loading. Default is Left Outer Join to lead object that does not refer to a related row. - classification_system = relationship('LucClassificationSystem', lazy='joined') __table_args__ = ( UniqueConstraint('name', 'version'), Index(None, grid_ref_sys_id), Index(None, name), Index(None, spatial_extent, postgresql_using='gist'), - Index(None, classification_system_id), Index(None, category), Index(None, is_available), + Index(None, is_public), Index(None, start_date, end_date), dict(schema=BDC_CATALOG_SCHEMA), ) @@ -143,6 +139,44 @@ def identifier(self): """ return func.concat(self.name, '-', self.version) + @classmethod + def get_collection_sources(cls, collection: Union['Collection', str, int]) -> List['Collection']: + """Trace data cube collection origin. + + It traces all the collection origin from the given collection using + :class:`bdc_catalog.models.CollectionSRC` + + Raises: + ValueError: When collection is related itself (cyclic relationship). + """ + out = [] + dupes = [] + ref = collection + if not isinstance(collection, Collection): + ref = Collection.get_by_id(collection) + + while ref is not None: + source: CollectionSRC = ( + CollectionSRC.query() + .filter(CollectionSRC.collection_id == ref.id) + .first() + ) + if source is None: + break + + ref: Collection = Collection.query().get(source.collection_src_id) + if ref.id in dupes: + raise ValueError(f'Collection {ref.identifier} has self reference') + + dupes.append(ref.id) + out.append(ref) + return out + + @property + def sources(self) -> List['Collection']: + """Retrieve the list of referred collections marked as origin.""" + return Collection.get_collection_sources(self) + class CollectionSRC(BaseModel): """Model for collection provenance/lineage.""" @@ -223,25 +257,3 @@ def to_dict(self) -> dict: """ return dict(name=self.provider.name, description=self.provider.description, url=self.provider.url, roles=self.roles) - - -class CollectionRole(BaseModel): - """Model to represent the link between Collection and Role.""" - - __tablename__ = 'collections_roles' - - collection_id = db.Column('collection_id', db.Integer(), - db.ForeignKey(Collection.id, onupdate='CASCADE', ondelete='CASCADE'), - nullable=False) - - role_id = db.Column('role_id', db.Integer(), - db.ForeignKey(Role.id, onupdate='CASCADE', ondelete='CASCADE'), - nullable=False) - - collection = relationship('Collection', lazy='joined', foreign_keys=[collection_id]) - role = relationship('Role', lazy='joined') - - __table_args__ = ( - PrimaryKeyConstraint(collection_id, role_id), - dict(schema=BDC_CATALOG_SCHEMA), - ) diff --git a/bdc_catalog/models/grid_ref_sys.py b/bdc_catalog/models/grid_ref_sys.py index ac3b98c..54bb615 100644 --- a/bdc_catalog/models/grid_ref_sys.py +++ b/bdc_catalog/models/grid_ref_sys.py @@ -70,11 +70,12 @@ def create_geometry_table(cls, table_name: str, features: Iterable[Feature], sri srid = 100001 opts = kwargs.copy() - if not opts.get('schema'): - opts['schema'] = BDC_CATALOG_SCHEMA + schema = opts.get('schema', BDC_CATALOG_SCHEMA) + table_name_ = table_name.lower() + opts['schema'] = schema grid_table = Table( - table_name.lower(), db.metadata, + table_name_, db.metadata, db.Column('id', db.Integer(), primary_key=True, autoincrement=True), db.Column('tile', db.String), db.Column('geom', geoalchemy2.Geometry(geometry_type='Polygon', srid=srid, spatial_index=False)), @@ -83,14 +84,14 @@ def create_geometry_table(cls, table_name: str, features: Iterable[Feature], sri ) inspector = inspect(db.engine) - if inspector.has_table(table_name, schema=BDC_CATALOG_SCHEMA): - raise RuntimeError(f'Table {table_name} already exists') + if inspector.has_table(table_name_, schema=schema): + raise RuntimeError(f'Table {table_name_} already exists') grid_table.create(bind=db.engine) db.session.execute(grid_table.insert().values(features)) - table_id = cls.get_table_id(table_name, schema=opts.get('schema')) + table_id = cls.get_table_id(table_name_, schema=opts.get('schema')) grs.table_id = table_id diff --git a/bdc_catalog/models/item.py b/bdc_catalog/models/item.py index 45d41b0..ff2d0ff 100644 --- a/bdc_catalog/models/item.py +++ b/bdc_catalog/models/item.py @@ -32,10 +32,10 @@ from sqlalchemy.sql import expression from ..config import BDC_CATALOG_SCHEMA +from ..utils import multihash_checksum_sha256 from .base_sql import BaseModel, db from .collection import Collection from .processor import Processor -from ..utils import multihash_checksum_sha256 try: import rasterio @@ -135,6 +135,8 @@ class Item(BaseModel): Index(None, tile_id), Index(None, start_date), Index('idx_bdc_items_start_date_desc', start_date.desc()), + Index(None, start_date.desc(), id), + Index('idx_bdc_items_start_date_desc_id_is_available', start_date.desc(), id, is_available), Index(None, metadata_), dict(schema=BDC_CATALOG_SCHEMA), ) @@ -302,6 +304,7 @@ def add_processor(self, processor: Processor) -> 'ItemsProcessors': Note: May raise error when processor is already attached. + Make sure to ``commit`` or ``rollback`` changes. Args: processor (Processor): Instance of Processor diff --git a/bdc_catalog/models/provider.py b/bdc_catalog/models/provider.py index 638d171..633d15c 100644 --- a/bdc_catalog/models/provider.py +++ b/bdc_catalog/models/provider.py @@ -19,6 +19,7 @@ """Model for table ``bdc.providers``.""" from sqlalchemy import Column, Index, Integer, String, Text +from sqlalchemy.orm import relationship from ..config import BDC_CATALOG_SCHEMA from .base_sql import BaseModel diff --git a/bdc_catalog/models/role.py b/bdc_catalog/models/role.py deleted file mode 100644 index 52f2cb4..0000000 --- a/bdc_catalog/models/role.py +++ /dev/null @@ -1,44 +0,0 @@ -# -# This file is part of BDC-Catalog. -# Copyright (C) 2022 INPE. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -"""Model for table ``bdc.role``.""" - -from sqlalchemy import Column, Index, Integer, String, Text -from sqlalchemy.orm import relationship - -from ..config import BDC_CATALOG_SCHEMA -from .base_sql import BaseModel - - -class Role(BaseModel): - """Model for table ``bdc.role``. - - The role model consists in a basic way to specify a required roles to access collections/items. - These values are used by BDC-Catalog NGINX plugin in order to validate user access. - """ - - __tablename__ = 'roles' - - id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(String(64), unique=True, nullable=False) - description = Column(Text) - - __table_args__ = ( - Index(None, name), - dict(schema=BDC_CATALOG_SCHEMA), - ) diff --git a/bdc_catalog/utils.py b/bdc_catalog/utils.py index 59cddf8..d6d7599 100644 --- a/bdc_catalog/utils.py +++ b/bdc_catalog/utils.py @@ -81,19 +81,22 @@ def multihash_checksum_sha256(file_path: Union[str, BytesIO]): def geom_to_wkb(geom: Any, srid: int = None) -> geoalchemy2.WKBElement: """Create a WKB geometry from a shapely.geometry.Geometry. - This helper uses the GeoAlchemy2 helper to ensure to create a extended WKB element (EWKB). + This helper uses the GeoAlchemy2 helper to ensure to create an extended WKB element (EWKB). It forces the SQLAlchemy field to load the Geometry into database with EWKB instead WKT to avoid any bit error precision. Note: - Make sure you have installed extra ``geo`` or the library ``Shapely`` before. + Make sure you have installed extra ``geo`` or the library ``Shapely`` before:: + + pip install Shapely Args: geom: A shapely Geometry srid: The Geometry SRID associated. """ from geoalchemy2.shape import from_shape - # Use extended=True to available the Geometry as EWKB + + # Use extended=True to transform the geometry as EWKB return from_shape(geom, srid=-1 if srid is None else srid, extended=True) @@ -104,7 +107,8 @@ def create_collection(name: str, version: Any, bands: list, Note: Used from :py:meth:`bdc_catalog.cli.load_data` """ - from bdc_catalog.models import Band, Collection, GridRefSys, MimeType, ResolutionUnit, db + from bdc_catalog.models import (Band, Collection, GridRefSys, MimeType, + ResolutionUnit, db) collection = ( Collection.query() diff --git a/bdc_catalog/version.py b/bdc_catalog/version.py index bc62534..fd7e024 100644 --- a/bdc_catalog/version.py +++ b/bdc_catalog/version.py @@ -19,4 +19,4 @@ """Version information for BDC-Catalog.""" -__version__ = '1.0.0' +__version__ = '1.0.1' diff --git a/docs/model/db-schema.png b/docs/model/db-schema.png index 0b874b5..d062691 100644 Binary files a/docs/model/db-schema.png and b/docs/model/db-schema.png differ diff --git a/docs/model/db-schema.xml b/docs/model/db-schema.xml index 72a6991..1141ac8 100644 --- a/docs/model/db-schema.xml +++ b/docs/model/db-schema.xml @@ -1,2 +1,2 @@ -7Z1td5vGtsc/jdfqeREthuHxpZPa7TlJmizX7U3uGy8iYZtTWfginMb99BckBgF7EAjmAeydF61BAknMnh/Dfz+d0XcPP35Jgsf7j/EqXJ+ZxurHGf35zMz+2V72v3zPc7HH95z9nrskWu33kcOO36N/wmKnUex9ilbhtvbGNI7XafRY37mMN5twmdb2BUkS/11/2228rn/qY3AXgh2/L4M13Ps/0Sq93+/1bOOw/9cwurtnn0yM4pWHgL252LG9D1bx35Vd9OKMvkviON3/9fDjXbjOrx67LvvjLlteLb9YEm7SPgeYxddIn9lvS+KnzSrMXzbO6Ns4Se/ju3gTrD/E8WO2k2Q7/xum6XMxKsFTGme77tOHdfFquFmd59c427y4+idM4uv4Y7B53r9yGa3Z+7ZpkKSHd8abkO0s3mTsDkmT5y/5xsJmm1+rr/38o7b1zLZ+RGnlsGzrK/t62d+Hg/KN6jGfwyR6CNMwKfbtL0+4atjDNn5KlsUuu7iG2Te/C4uLbltwIEg5vNnECOPsU5Ln7C1JuA7S6Hv9/EFhoHfl+8pDP8dRdkbTKGaTY9v7Q4q59MayjPo59t+1OKxqCs0zsQPZmajdONP+F4IzZX9UftFh187U+GZHxZtdT3PqZ5vMfEjFeA6m1GI+panWDPVgt22mmhnX78WFOPzui8PePkboQBskhqPKCInhNmzHc31ZtlNMre/B+qn4vpfvCbCn/Gp9CL5ld56alQTr6G6T/b3Mrkg+yd9+D5M0ysh+XrzwEK1Wu4uehNvon+Db7nz5MD3mX333Y+y3Z/bP5bDkJwh/1C5ccdspDj7AvjoQlD8QxYmMhWN4Vu2SFtN85EiZzoLda9lQOfVzxLe323DsGNlgjJbxep1dkyjebMFYbf+OHtbBbsLexpuUTYZ8vJb30Xr1IXiOn/Lrlk3n5V9s6+19nET/ZO8PaggoAJHNiOo7fs+PLEZyN7LhZzYYpLHrY/Cj9sYPwTYtduQ/InjcRt/K7/eQmXW0eRunafxQvIn9mhwn7+J1nOx+JL3d/eOZ4O437781sfLt+oHUsH3T3v28JP4rrL5ybluUFGfgfVSrhfZAQnGA5TRmNiG02PP3YelD/WLffWXZ47C7IM88a8Z1xJIcYEnZK9mEoufZf//92/XFLxdX0KDug8f8z+xnplGwvson4+ZuN63T3e0kH6d1eMvGNSm+dP73NzaWVVzs39sOi+qQbYpbz2OwjDZ3H/afYlmHXVfFp+W74uyUt+vdDek+O1m4yb9NnAZpK3neZlf6XX5XyRhkvsu2yWF7h6XH7CbyLptmaRJEuzEOMwP+O8yN+O0qiR+v2T3CaMw3u2GLZl8LsvkWxFb3Tj9zocZ4a3GBtXx+P9o6vlVnN2ek+xpJwyJEGUTXmOc/szDn5ujWaTvmdubwTaAy5pakIffAkG+Ch7BExJ/nV+9+Pb/6ybTtf8nhhNFtAruTzRwSYqFg94SC6Yy3EB9YyB+cBaNiKuxNYnpIOD7lO8bcG0QBEWPMlKHKIKdRukYOTJwDnqmOA4QAG0EKSKAAMfhDrgIDJhjiVbhdJtFj/uSZvXB98eUap/9kpj8xPIXzn+L8P2n+D3wW2E9CPfMfioNp+JBdhGB9s4yzP7ZRDoKb7fI+fAjKxcF/fv/021vEwnSwQC2FWIBiJWJBBhYsfViAKiKjQXhz+7TZ6dI3qoRFRMIQJDg9VWch9gJ1RJ6PCakgWEUkw2REIUMOdcQ89uQmCW9vts9bhMOk4eAplBMJ1BMv3+OSQT4c9KmLJlQXl+tgu41usyHYP1A8b7MHjZwSiIhJIqKUhlQgwoRK46V2P+SMCDFQeDT1CY/smZEb8XKTPj8ePBEXv/3xEcEwHTBQhRKkiRKkEq3B1CdBcuITs3MEq+wqod44VQY4CvVGTnAkMkAGA/TpjZyoxb/C57/jZLUtGXB+dXX+9cx01vk1KKITss27tPzdiIaxaGjExZaRtCKQ4SvUIzlxjYgMGcjQpz9y4hgfM/POr3p4gAYuHOZBB0oUCpKcAEekgww66BMgKRQgt08PD0GCcJghHCyFUiTFoEclcKD6tEcKtccod0YE222YIh5mhwdHoSBJUZBUgwd9giSFgmS0vQm+B9F695UZH95++vTh4vw3JMT0CeErlCspypVqCKFPrqSc8MggDe/iPN0fXZaz4YJlKtQkKWqSSrKpqD5NkkJNcldi42aV0aFcN1z/++PF79fnHz9f/y8yYjIuTctSqEBSVCDVsECfAmlBBTLcrJAEMyCBq1ButFBuVEICS5/cyNTr6qrgMchH8ib8ke6+YQ6DXy4+fby4vvr60+dPH77+8um3fHKYhkVNB+svzOBRwlcoQlooQqqBhj4R0oIiZPZlt/uiDJgzMc2Vg20q1BktqDNimSZJHNAnNVpQaiw4cPOYhKtwGW632fAgEybLBFuhxmjx8rARCbLzqCx9kqMFJUfGh+0TwmHqcHAVio4WLw8b4SAdDvo0SBtqkMskDNJwhfrjdKHgGAr1Rxv1RyWPELY+/ZH1W6kM8dPjCikwcQqYCgVFGwVFNRTQJyjanKjGNHzA5iIdzUVAExHDcC7OL8/am4g02pEYtk38i75Q6Ggu4pkLyz/8KxBRNqDiOSGYTlg1KIt1lBtlUVCAnGWrEdeY8E1ldKuRFunySK8RnsGIaDxhQynztGYjTRy9suYjrePdJ1rWblltVAbekTXuUJScXseRl4WL4WvS47jgdSGRhguobCIsesPi5FCHXhBx9UEEipeVemETKjaKIOkDEl79UVkgcaAKemJxYmSJBJb42ljiQM0zjdYhUmRuFCG8qiHSMAJl1BPLGL9yjIzChdMijKjABRRH553VM2lkiEYEL62HhwgRGqsDNdbRtvCiZfShMGhpBt4BAyFDDEXPOaf1vCoU8PJ6pKEAip2IAhkoGKZrChliqGvW6oTMqUjIXDgwLoOngw99vWdCjGecuol86MuHYZKlkCHmSJbr+Gl1s8wvX7la+PnTH28/XCAgprNQMHlpPLJA4EJ1EkEgAwTD9EYhQwz1xjmWGnxVCOBl7UhDAFQWEQESou/cYRqikCGGGuI8m5y8KgjwsnOkQQC1QzUQ0KcdulA7fEzi79EqTKbke0QSwAKivJQcaSSA0uHle4owkJ2o5+qTEl0oJX77Fv8okTDj2kCvChO8nB1pmEAFUc2CQZ+C6EIF8TaO08ck//HIhnmxwVYoKnooKqphgz5R0YOi4jbBp4hJI8BTKCp6UFS8fG8jBWQ/RXj6REYPioxzLffxmrBgGQplRg9lRiUrA0+fzOhBmXGu9T5eFQZYWzolGMDwRDUY0KcpehxNMdissOBHV8GP4gQfog37mEZFjzLqENQGoee2RZkR8T69l9EcrwFCDNdf+O1FQEhpmxUjM31v4XIMzWKPCaMsDSqRsywE4kz5bjO2EIjXImO2FwJpsxkROVUeVDaxFoiqUDivuyIlK/Qgetx9KEwWtUBmVgVkDqwYvFLtYAWnCohMVvjjSlQiKYaTwu+uWimNFFC/XMYPD/HmZnrFg3o+zU6aGYIZwSnw0cYIEQ87PhawVAMEcxAQhAwxVC5X4XaZRI9ptSvO9cWXawTBdEBACJS1pJIA22+rIUF3TxxpJIDi5UO0uSk2cwj89sfHi6t/v8PnCGnPEQ2By9j9E4UMCsPopD5gjOvMjQ8YIyjS3TlH2gMGVC0fgh9IkRdDEUcxRaCkiRQ5kSIDvWy+PkGTGBxFM97l9SFD5s8QTkkQmQwhBmqduiBSzGRNFIFq5za75uHNw9M6RZJoIclpxYW6/Kuc1CC5JBmniCJJhj/UFLNZE0mgTLonSbBaIUheAEhs1SAZJ6giSMaAZJjIKmbcoco6944Lc+WJEG54qrnB60WObReUoUOfskoMKK0m4TZeP+3Q8bSJ0inxA522MFOR9IeFCI8eMaB4emJzhdfkuOVkKw7GxDDpVMigEyid5sLpTfr8eAjwusgeVxANE0KDBesYSEUDwQ7lGrhAhomhYkYciqEP0UO440K+cMBVg/TC6kK9KJTVOFaGDKh9YgU1JdQYJnyKGXQofJYlVudTX3UuvBDCBV9tkDghGBuqviBKMTE1QYEjYk6tJArql31gYbG5qkq/JOKiQg1J8Hix4iXRKF4SKF5OrnwKIqMXMmzVyBAXAorIOBUZGmNATShk/t9TtPxrHcd/QQPAcivVcitttVUagsTl7l9fMHRUUCFWXkHFNti/4gysggp1OHoFdRe8xxJDxCrVhBKnBlc71lLpbV5mizp6pJgK337E4AfqpXkxlbwMNHrc1cUPt1FHyR0IKp5JyLjxzjw7166Xvyx0DF6ydqGDV1tFJjqgaHqi3x2RMQYZ3VVC5SEDiqF3SRgeKikgMWZBDF6lFZnEGN2dCIkxhhjdBUXlEYNTUTT/C4ExK2CUThFVxICC6uV7C4mhihjd/YvkEYPTAh3db/OkhqWWGhRqqkORgVr6Ych7aelmd1sjacigUAtF99tMkcHpnywVGVAHRWQoQgbVKH5S+PiZxE+bVY6M3TXKLsh9fBdvgvWHeDeo+e//b5imz8UFCJ7SONt1nz7UvGvnSbK7+BdXxdzOd+aeruK84WZ1eMs/YRJfx592b8xeKN6Wnyv8EaVfKn9/zQ9f2MXWz8wBt9t4Lk+dXYUv1Y3KUfnm4bDdVnnc6i5kRnz42ReHvcU45++rjXLK8HBWcUj18Kkl4TpIo+/1k/GGszj0c7xvXlnYmGcXFsCcb67l1s+xjZ+SZVgcdrAKcCbiNE9lO379VPvfCE6VjWHwXHlbMaVav7Pvtnzntq8GDrCpf/y3lLkxw4+gpHZE9sf+dx7mVDmKPacZTyuG7qXcsj4E38J1fUIBR3ArGneO6KOEG4mqFtG1OJWxcAzWJYuJ8vutkaZuOgum1bExajiP49vbbZg2Ru30cYJd/qaFw6GI2pOg4IbLwxZvuKVgi1Awpf3Graw3t2yjeSqjcSpB3CImex4v7a/40q3fzaLG2CPM+hECOMRToOFKb/ocapFyZXPojVsfINK44QqikIsUUkChQzQjG07PoQNBRG1vUboF2eksdn7RMLKc5iqqEy3NI0xHNFp4UjV8upg+Wlo0X+looVZjhBqnEMQWH9mihS2kjJUUBBdLFlz8Bio80gWX5hFMmxAGFwsq2jw/2PTh0qIOS4eLXX96YgmfYtliwSdabWz5GGye5cDFhmwpK1TKZ4tjmoArA5+e4KlIcz0riClOU2AhrFBB63frOkIAU0wOU2aoyVjHI/blMYWQBXWq3VTrD0fWwjCrrzYERUHImZB+/WKR4zafeocjB5yqzBkUjZzmmoQQ5/gypvMIAcjpFzI8feTokoGNhVH7V/8QNcSBsb+HhKPtzWMSf49WYYK9wk9LXsuOMpfLM05j8JXzzbGdYzbbP33tjdVUcTn5apStPare1tIhP8rdakH9lRkM5qqpzFWrW1nx0/oZmNWiPbcnsHENSoj73oJhxZi+pj5O1NJYc8GCuismwOqBihB+cLLYJPIDBhkf+AEXpsgPSfzQWIDBhtIqxplPNGi0Ax2cdDZ56LDFtdvCiNGz056/bY3ttlhhoGq6fLwOt3tanF9dnX89M52889bbff1p5y4tfzCyY2otc7qYwst4kwgVcZ23Xi1Uhi5C9hNbE1SgMIqZK/NchPCS3SQCQ1yHrVcLjMGrEI0dttiJRfr9+rr0evoHZSWhsJQYY+Hs5H+WFsNSZFqSYrKNz2ESZZc6l+N3+/aX8Kjn0XR4OS5sisv3Pb4xmxHeLBrpZN/jmzKKqBT/SSPcs8X3OMRNZPdr4TV5V5/dIm2Wrj7X8uteuOLsowe+6SmWErLEOkJOnyNENEdaZn99oisLMoAhzc7AeU5tf8Fiew5uPjnpbNSx+d+69cs1D7A7IrdLALccMD4owe7XP2z6pGoRUaWTylmYHifsoASXlEw1NjumDy5pC6BTcoL7LHVYBkONgMpWOsTJC+tW/jXSAfyh657MOAEPXadf8Pggy4TC8CxXPk6LyPpCVj4ORPzEACIFBEOx1AMgxOWknbCZJx8glBwHiOUNBIjVfG4qdwheTVnNZz3qH8/cBwc45vHVFFhlNg4Yv5pyJh+QzOSDIZNG1UNI7b7M7sHVacWMW0GVjabJeM1nid55XFazygYx7H4zaZAlvpBgY0dTsPEbszFaUsKJHShgp1HuTm2OE4YP18KHW23qlGDgWjQ5bWZYmixhU01ssANjgzF6b3LRe86Eon8dfvRvfwvBoL0x9yWNQb8ODPq9S6LVTRLe3myft3nY765zBRaXnpLnvIMdSiN/HZ7qu4/8/QMzB9RBRGPkLyvDVTGBTfBwqEn/5/nVu1/Pr34yjX8hOqaNDqWRvy4UeBEZ6pDhagz+dU0w9JgsME9kqA3sdcUF9iIwTgWGqREYGNj7YoChNLDXFRfYi8A4FRgaA3tdqIJWlQ1oAyiLC5bFLaPmRnatph+ZeX+rrSlMnizuiZDFXShyzlIWp8K4MEVZ3G0RQ9tlca7JiEEIFEdPk8Vf2c2izs1xd47uhmlU2rBDRZMvZ9m2Pj1rd7KZw0I0HHh9m3lwMEXcTjwoeupGw94mpseF4/O+c9BbSt12oEDMIEOFchVul0n0mBfFKYlwffHlGkkwIRJ4pkoSQC0TSSDjcdLr7m8mjwRQd9x92xvu0oBq9HS9MB6MK0rRwQlieCpBATVMBEXvR4mh6wevJexTCTU4NV6n5uZ6YbwQwwXWQ0UNF6BUiVyQsoBocUwoQQGUHyfnwEIUcFDAKewsEQVQckQUSEHBMIFRzCBPqHUWP7VLfs2ZM6Ep19yMyTK4WUHONStiWaZ8NPOxehedgP315FSceNPsEdqVIwkOIKKbc/lQUr2E/pbJ55l5unpzkWYXSrN+CjGJZv7km3MNpdeQLHARiallxlaVXk5/f/vYfG+7nu9tNxsONr1qpxTQOXZis1GPQGDOqg812VnmrPq6enK5C1qrAdCoReMt5LBlNknvJRe+VrEweP3C0tCrBKCsiZ58BPgGXViVwW6sZghpnLF3prrhG4vsCcp1iWtYxDPM2on9fi27BlkS1Fofk3gZbrcxtlTqbKk0SnM/Sixi2HTBYvPKCPLCLLoCQk1fxOOXD0XZWQaCzSGIeHggmN8SO6ojP9qHAi3mRyuTavxu1VZaFLEPVVuewzf76yfHwuxG3ZkHHdBQmhjtj5NysaL4GGR0q7vykAHDR2+za7mO0ucmNvSGkCI0ekFDZUp0WaQcoaEBGt2BprKgUVaIqoz7OvwergExqInAmDgwlCZEm0xSQWIoJ0YxazURAwakZj9jWw1LR2bMhxkqc6JNY1wgKjJjDDO6w1HlMQMqn09JhE8lcwSGqxQY4yJUERhjgKFP/jQNKH9mJwlW2XXaI+M/v3/6DVuyTpwV5fRVwwrUPfWxQp/uaRpQ95xcgDsCox8wqEpgENQ89QFDo+ZJoOY5ueQ4BEY/YDhKgYGSpzZgEI2SJ5EQHSqpVeHC3gXSldHnZEEMejY6eyZ/f7P58iEklVreWaWN84JQeqaqlTPLdFbQyZlaRq3fSiNY1XEbLQl793XO46ltr3LmRsE6u2nFolJx7EZcLLGPdx9zGklCjfePzsQxCRSIL9/D2Tf18PmCGEfC5yW1XiRGZqLNIGoZ3RdNAiXd7KLG66e8QNHN0yZK4a0Rg50FV7oklms0BpuyjMTOtGFDQExz2dquHtOMAc3TCWguZuopAc3SKluaBBs+6VxD62v4ZBKozrYFNGO7Fu3P3V3Q6FvxUozpQIF3KDLwsftUZOhr73QotHUY9+3zw7eYBSfuG8TV0EGRHBMnByeqWR45TKj0IjlOJMfA8nfF+zSRAwq1kyufi7zoxQteULNEYIjr8vRqgTF0qbG/1JqAAXVHdAnOFBiciGaJwBDX5QmBcSow9HV5KlUzDDqaPzA4Ec0SgQEVUASGKmBo1D9NqH/mbqbsO+f+sm2yhFaA7rJabaD6gWVtoMyE4r/C6ivntkWZ8fA+qhc1jjvebMPNXbjlv2JwD144ZjNVpvjWwobWRSxThHlBjbRiXvMsMyQMOGq9cnWDLH5avzuV2aK2HnHV8Y1KCLIo1F9zZ927y9N6pb+yW5S4DhLFpD56v3JkrVMoJ0i2dr9SBpUeK92e9eMnTZSTl7UdsOC56PiwEFFb3KRQci1gcdL69jUVkhdICtodHcsjhZiR52imO9ccKmgTfSDuIAeviZ3MZQYWBdD2REy7iwLIW2FwpNMdN1BImyc3eOV0pYIDiwPoA0d3cQB54IAS6jJ+eIy3URre3D5tdo8oWGt7Tnoas7FD4XeWHtFDQPMtEUYF9VlUzSYXy05b6gxoEsig6HpaNPsru+VwHngH33+641Kl3X8sqItymzHrLMz9KhWxDjooVsSsccGnr0kJOz7xu4bd6o40lSZ+MefdlCNNkQVjNC4xZjIurvT1sWDoumA/HzWxAAqZ2RhkpwQLA1wXTIkFhFWOUwUDKHz+of2xYao4EOgks7rjReWxgaNVopNMIiRGi91d0KC9BSoxD50YN6pvUdEdNypPbOBUNUUv2ZzB4SgGB2bP6wOHRpXShirlYxJ/j1Yh9qE90TeWHWUul2cc39jK+ebYTl80HPeBvXFI0wtmsPJOnQ1qhRRzsqFiOU8f2AzuL8N9YHaL3KmhQa1pQ/kT6zkpE75tjTVRbahoYoPa6S5KO6ChskGtaUOd9DT9C6ExBhoa+0DZUPmcnLcMidGLGEq709oY6amPFxrbQNlQ9HxK1tg0TiEsGkGfZZioEIio7VhrY4MofRTR2CDK5uTGo9dtlqsOte1qHSihIjBOBMZQ6dzW2CDKgTooettmCgyl7WodKIciMBQBw9EohjpQDF0vl9vFch1stzfb520aPkBDQJ/bEZ/bKgi9W67PzVl64TdBeWe+5S98v72Ok+v1r+PkeCIsCWqj83TBCQPKbIo3OS3Cqp7cNAdKrZibpibO1BkWZypm2KFSGjyl+R0hfb7ZOejq3jlNq9dXGYbeAQjF6WnOuIBSjEEfxoZhoaRiRhzqn9nX3eaeN4TCRKGgOE/NGRcsilAYBoVhYaJCRtzlaJxHElO0wQH1rl68OCGXTch608UOS9oUL3dY3ruYceconZiWMmdwUMXgwE5L+sBhagSHBYY57+bOxjK7HPfxXbwJ1heHvdl1fNqscqjsruLhPR/i3bDnV+i/YZo+F5coeErjs1pn+HCzOk+S3eDE33PlPN+TS9XFKXcaOnvHxVVBh3xn5U3ZBUyev1Q3vuYbC5tt/vyj+uLPz8XW/gfnv7I2VNv4KVkWu4pZlrJpf1aRcnqo2SO7xnvN8mulEM7Osf+uxWGH0c6uWPBceVth360f5PqgzluRpnTZcoRvjD/CIaRhovvvfTDY8lL2tGGorR7cPSFmV03R00Mswz7q6vH8/vqbJeTJC0q16OqZh6vHPd5RSrGrx4VKLrp61Cg3rsbKAC6Uc/kZWCjnqpNzO8ig2MfjopyrAwoa5VwPyrnLeIVQmDIUFPt4PKxLqgEKnsYipczAOtIukQ3TY4Pq2oQeFirVAYdhgqyYEYcRp/uw5f03zNv8YY+/qdKhfxFCMbYChc9L7UrDa+CDxsqlHpQpq2kNyIcp84FZhSo+QCUS+aCCDxqjSr22AqUYQDbJOJAuZHAKu8h0XnhYnlRbHIinsTypD4VKDCCbMzhMTi0XmeDwMfJUGzh8jZGnPhxmFQFkP6K0DP3K/q5EfmVbh8CvfIPFfZXxYuTspHgxGIx2HX8MNs/spVpI2qoZtVYPbNtfq6OxZ+UTnvTos/5jTMEYjx7BniF+lev5T5jE5ZU/XNbdaHZaXMuFr11j3xZ/jVsi/Fg1cBbuZBNaP0dLhB84EzGIsXBYwCI7HXUbPVj3v3N0wKDvtHxvYbF8PpQ/L9+zWVJHzIfgW7iuGxWIdWu9Bexi7Y6SfCSSW8K7i1NlY2YwpZfFIuy3RlrWG6t+1jdOA/Lx7e02TM+a0//UYYLKYxo9hOsom8bgro/hltVwy16ru+PBk55rLAzSmPK8KDeTc9cnQqIlfY4MmV2+cNfsW40IiTXqz0ZFTfqnR03y7EnMKpIfM/nuNLkSC1GPuWN1i5bSCjb5ULTMbyc30Sb/zUqUB6SJKHDwgirlgQPqldjxQh0zuhVLWcygBlQssYzsRLXKDmLwfByyiEENFCp18aKYs5p4YYJxR9/GPHnBq1MvERiYGq8PGKZGYMDU+BeielfdDSxGqKqEUxblLF8JJ6ZBFrSZ/EtY5eaT5XCzqYuRMutKsBaefVQzs900ra5v13HIaAGdGryY0PkJ6MXs0yCgk8wenWpmulv7FGthmNVXG8YlRF6nrH0ookcqemxHKHrYnegwub3GfUgYelzIkeNlOLoPEYAeXrgpXCtNHz0tSqhs9GRnNmr/6h+ihjxQif0WbFY322QJBhIde8fqqJStuUAdFXpuW5StlXkf1Wq+J9RXyUx0YVrt9VUsZvo9SulbZrsp919OQ6V2Z1nZG3aBhrPzGAp7upqLx7CAw0keQ3kRh2XbGOgzhMtdrLbChl9cmkMxo7WEHVICVVx2n1JJlB6a3mtMhOoihdq6K5RA6bcgxUlKHmZDDcIE6e4EJSsbihKOdnskGwqlf93Sfxc5+hdnEXObgeGop1gIav8jHsNJSwitkuUFVFExHWrO3CBEMThgfCyCQxU4WrzHSsABNdAoL8vwmMTLcLuNEyxH3CWjjelw3yWK2XThNlV6JrN2taQVEjhPCRRYo6JuxwxrDc/gliJYA9tP8FM0MGn9jSnhB79mBxonimAYBTvmAVdjFKwJRdDyVoNMmVzsfBc+OMKYPHyYUEM94GPwchXxcSI+TI1BseypBIPop/+E24EOThC9RHRgTKw+XpgaeQH1UFTE5skLXhC9RGBwBFUEhiJgdJcrlQcMGMmqtr9caxjrbu7XY2L37/zEjYzd/4zjXeMKONbiWZkgKDye9eTaLc2uck6tdMup7x8fLmpCqfQhT/pOnx+xiIj8IiLE8hoDTAybgFuCvBoi1ORIoVg4ZFJyhXmy2ikvD8/EVH+diwiNIieFImettdr1xZdrfNzQ/LjRgQqVVUEoxRx/baCgGuVMinLmS+GF0pogFOVMfbzQKGdSlDNfCi/U1gShKGfqA4ZGOZPOSc7kp/ifdQuZpTJTVTI9WUImzFWnTWXKdxuDJyqR3iJAAyMdifSdh4xXRikvkR7WYFeWSD9wpnbpj7TtQp5UroM2T1I/hZi0eAqlykO54+1NEq9DDOw9LT8+O8pcLs84+fEr55tjO30XBh2lspskYfVxqssEedG+FGqWWCZ7XgG/9PSkd2ledKsl5f0MA36VrkC7E9+lrUAtKG3mdx9kycQ8Zx3Y4Mnh8rABZVEM9FWPDas7EV4eNqDCicr4RJWuDnTwlHF56MAEeH286E6Al8cLKHCiMj5PXnCVcXnAwMR3fcDoTnyXBwwXDLMS3bt/aVvWMnRhGKyFaN4A9E2+wz072jk02/gcJlF2VXJlq6+MbvLigZkiJF9Gt5oVX127MdB9S9uabqOott2zpvYQqdWCUqvWKrKDJ2OLv7KsIutafr3Aa3GWsQWsnYXpcerElgJ5Q9cUI5BbUOhEUXyqovgbs9l/1nN7rhGIK0IWt6GOifqVHi18Fd4GT+u092LTatE+dcjhNtRCMSJc2WrT1hjoaUNFcxcRvkPIn+dX7349v/qJGv+SgxF8NhWFC6UyuA010D+Ge83w6fRUXpgaeQFlzFW4XSbRY+6IxyySufBCqfZtiwvyxMXFqbDQGORpQwkTfWXz5IVa7duGcZu4ulAFDH3t66kNJUt0ls0UGJZSYEDVFIGhChgaE9pZ+enJO8uqjrJ84h71km2yy/ClulE5Kt88HLbbYsftL8XxhBQmGNdcaVZ/4Xu8K21BvVaXimc7C+KZZd8+s37+vm422uwO69nWwqPEt1wrO3nZZlCC082BSuosnW5Oiyb5Epxu2WYSx2n17UnweP8xXuXjcfH/ \ No newline at end of file +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 \ No newline at end of file diff --git a/docs/sphinx/tutorial.rst b/docs/sphinx/tutorial.rst index a6b14ae..223d099 100644 --- a/docs/sphinx/tutorial.rst +++ b/docs/sphinx/tutorial.rst @@ -193,21 +193,53 @@ In order to search for Collections, please, take a look in the next query. To re .. literalinclude:: ../../examples/query_collections.py :language: python - :lines: 24-40 + :lines: 24-25,27,30-41 You can increment the query and restrict to show only ``available`` collections: .. literalinclude:: ../../examples/query_collections.py :language: python - :lines: 24-34,42-48 + :lines: 24-25,27,34-35,43-49 A collection, essentially, has a few unique keys. Its defined by both ``id`` and ``Name-Version``. .. literalinclude:: ../../examples/query_collections.py :language: python - :lines: 24-34,50-59 + :lines: 24-25,27,34-35,51-60 + + +For spatial query constraints, consider to use special `SQLAlchemy func `_ to generate SQL expressions in runtime. In this case, we are going to use `ST_Intersects `_: + + +.. literalinclude:: ../../examples/query_collections.py + :language: python + :lines: 24-25,27,34-35,63-70 + + +.. note:: + + Remember that property :attr:`bdc_catalog.models.Collection.spatial_extent` are updated whenever an item is inserted in database using SQL Triggers. + + +Update collection ++++++++++++++++++ + +The process to update any collection is quite simple. With support of SQLAlchemy, there a few ways to update a collection, which are: + +.. literalinclude:: ../../examples/query_collections.py + :language: python + :lines: 24-25,27,34-35,51-52,71-82 + + +If you would like to update several collections once, you have to adapt the query and then update in cascade as following: + + +.. literalinclude:: ../../examples/query_collections.py + :language: python + :lines: 24-25,27,34-35,84-91 + .. item_: @@ -257,6 +289,15 @@ You can register this item as following: :lines: 26- +.. note:: + + We strongly recommend you to use :func:`bdc_catalog.utils.geom_to_wkb` to convert geometries into WKB instead WKT to avoid floating precision errors. + +.. note:: + + We also suggest you to keep the ``footprint`` as simplified geometries. It optimizes the queries while searching for spatial areas in PostgreSQL/PostGIS. + + Access Items ++++++++++++ @@ -288,6 +329,14 @@ Since the ``BDC-Catalog`` integrates with ``SQLAlchemy ORM``, you can join relat :emphasize-lines: 16,18 +If you would like to make a spatial query condition to filter a region of interest (ROI), use may use the following syntax: + +.. literalinclude:: ../../examples/query_items.py + :language: python + :lines: 25-37,66-75 + :emphasize-lines: 14,18 + + .. note:: Whenever the entry ``Item.query()`` is used, it retrieves `ALL` columns from :class:`bdc_catalog.models.Item`. diff --git a/examples/query_collections.py b/examples/query_collections.py index 3c65dc9..673bfec 100644 --- a/examples/query_collections.py +++ b/examples/query_collections.py @@ -22,10 +22,11 @@ """ from bdc_catalog import BDCCatalog -from bdc_catalog.models import Collection +from bdc_catalog.models import Collection, db +from bdc_catalog.utils import geom_to_wkb from flask import Flask from shapely.geometry import box -from sqlalchemy import func +from sqlalchemy import func, update app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:postgres@localhost:5432/bdc' @@ -62,8 +63,29 @@ roi = box(-54, -12, -53, -11) collections = ( Collection.query() - .filter(func.ST_Intersects(Collection.spatial_extent, roi), + .filter(func.ST_Intersects(Collection.spatial_extent, geom_to_wkb(roi)), Collection.start_date >= '2022-01-01') .all() ) print(f"Collections Filter ({roi.wkt}): {','.join([c.identifier for c in collections])}") + + # Update collection + collection.title = 'Sentinel-2 - Level-1C' + collection.save() + + # Alternative ways to update + ( + db.session.query(Collection) + .filter(Collection.identifier == "S2_L1C-1") + .update({"title": 'Sentinel-2 - Level-1C'}, synchronize_session="fetch") + ) + db.session.commit() + + # Mark all EO collections as available. + statement = ( + update(Collection) + .where(Collection.category == 'eo') + .values(is_available=True) + ) + db.session.execute(statement) + db.session.commit() diff --git a/setup.py b/setup.py index 4048d9d..1a87840 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ 'Flask-Alembic>=2.0.0', 'GeoAlchemy2>=0.8.4', 'py-multihash>=2,<3', - 'lccs-db @ git+https://github.com/brazil-data-cube/lccs-db@v0.8.1', + 'bdc-db @ git+https://github.com/brazil-data-cube/bdc-db@v0.6.3', ] packages = find_packages() @@ -116,7 +116,7 @@ setup_requires=setup_requires, tests_require=tests_require, classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 3 - Stable', 'Environment :: Web Environment', 'Intended Audience :: Education', 'Intended Audience :: Science/Research', diff --git a/tests/conftest.py b/tests/conftest.py index 0f4e79c..17c06ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,8 +18,11 @@ """Config test fixtures.""" +import json +import os import subprocess +import pkg_resources import pytest from flask import Flask @@ -33,12 +36,33 @@ def app(): yield _app +@pytest.fixture() +def fixture_dir(): + """Retrieve the base path for fixtures.""" + return pkg_resources.resource_filename(__name__, '../examples/fixtures/') + + +@pytest.fixture() +def json_data(fixture_dir): + """Load the fixture json data in test cases.""" + data = {} + for filename in os.listdir(fixture_dir): + entry = os.path.join(fixture_dir, filename) + + if not os.path.isfile(entry): + continue + + with open(entry) as fd: + data[filename] = json.load(fd) + + return data + + def pytest_sessionstart(session): """Load BDC-Catalog and prepare database environment.""" for command in ['init', 'create-namespaces', 'create-extension-postgis']: subprocess.call(f'bdc-catalog db {command}', shell=True) - subprocess.call(f'lccs-db db create-extension-hstore', shell=True) # Create tables subprocess.call(f'bdc-catalog db create-schema', shell=True) diff --git a/tests/test_cli.py b/tests/test_cli.py index 2a2fe42..a59b4ee 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -21,9 +21,11 @@ import subprocess import sys +import pytest +from click import MissingParameter from click.testing import CliRunner -from bdc_catalog.cli import cli +from bdc_catalog.cli import cli, load_data def test_basic_cli(): @@ -38,3 +40,14 @@ def test_cli_module(): res = subprocess.call(f'{sys.executable} -m bdc_catalog', shell=True) assert res == 0 + + +def test_cli_load_data(fixture_dir): + """Test the BDCCatalog invoked as a module.""" + # Test missing parameter + res = CliRunner().invoke(load_data, args=[]) + assert res.exit_code != 0 + assert "Missing --ifile or --from-dir parameter" in res.output + + res = CliRunner().invoke(load_data, args=["--from-dir", fixture_dir]) + assert res.exit_code == 0 diff --git a/tests/test_models.py b/tests/test_models.py index d4907cf..5c4e4dd 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -23,7 +23,7 @@ from shapely.geometry import Polygon from bdc_catalog import BDCCatalog -from bdc_catalog.models import GridRefSys, MimeType +from bdc_catalog.models import Collection, GridRefSys, MimeType, Provider @pytest.fixture @@ -57,7 +57,7 @@ def test_create_grid(db): with pytest.raises(RuntimeError) as e: _ = GridRefSys.create_geometry_table(**fields, srid=4326, schema='public', extend_existing=True) - assert str(e.value) == f'Table {fields["table_name"]} already exists' + assert str(e.value) == f'Table {fields["table_name"].lower()} already exists' def test_base_query_methods(db): @@ -75,3 +75,21 @@ def test_base_query_methods(db): db_mimes = MimeType.query().all() for mime in db_mimes: assert mime.name in expected_mime_types + + +def test_provider_creation(db): + with db.session.begin_nested(): + provider = Provider() + provider.name = 'ESA' + provider.url = 'https://www.esa.int/' + provider.save(commit=False) + db.session.commit() + + assert provider.id > 0 + + +def test_collection_methods(db): + collection = Collection.get_by_id('S2_L1C-1') + assert collection + + providers = collection.providers \ No newline at end of file