def commit(self) -> None: """Commit both reader and writer sessions to keep them in sync, rolling back on psycopg2 errors""" try: self.writer_session.commit() self.reader_session.commit() except sa.exc.IntegrityError as e: self.writer_session.rollback() self.reader_session.rollback() logger.error(e.orig.pgerror, exc_info=True) # Explicitly catch foreign key errors to be reraised by the API as validation errors if isinstance(e.orig, psycopg2.errors.ForeignKeyViolation): raise errors.ForeignKeyError(e.orig.pgerror) raise errors.DatabaseError(e.orig.pgerror) from e except Exception as e: logger.error(e, exc_info=True) raise errors.DatabaseError( "Unhandled database exception during commit")
def context_session(self) -> Iterator[SqlSession]: """override base method to include exception handling""" try: yield from self.get_db() except sa.exc.StatementError as e: if isinstance(e.orig, psycopg2.errors.UniqueViolation): raise errors.ConflictError("resource already exists") from e elif isinstance(e.orig, psycopg2.errors.ForeignKeyViolation): raise errors.ForeignKeyError("collection does not exist") from e logger.error(e, exc_info=True) raise errors.DatabaseError("unhandled database error")
def all_collections(self, **kwargs) -> List[schemas.Collection]: """Read all collections from the database""" try: collections = self.reader_session.query(self.collection_table).all() except Exception as e: logger.error(e, exc_info=True) raise errors.DatabaseError( "Unhandled database error when getting item collection" ) response = [] for collection in collections: collection.base_url = str(kwargs["request"].base_url) response.append(schemas.Collection.from_orm(collection)) return response
def lookup_id(self, item_id: str, table: Optional[Type[database.BaseModel]] = None) -> Query: """Create a query to access a single record from the table""" table = table or self.table try: query = self.reader_session.query(table).filter( table.id == item_id) except Exception as e: logger.error(e, exc_info=True) raise errors.DatabaseError("Unhandled database during ID lookup") if not self.row_exists(query): error_message = f"Row {item_id} does not exist" logger.warning(error_message) raise errors.NotFoundError(error_message) return query
def item_collection( self, id: str, limit: int = 10, token: str = None, **kwargs ) -> ItemCollection: """Read an item collection from the database""" try: collection_children = ( self.reader_session.query(self.table) .join(self.collection_table) .filter(self.collection_table.id == id) .order_by(self.table.datetime.desc(), self.table.id) ) count = None if config.settings.api_extension_is_enabled(config.ApiExtensions.context): count_query = collection_children.statement.with_only_columns( [func.count()] ).order_by(None) count = collection_children.session.execute(count_query).scalar() token = self.pagination_client.get(token) if token else token page = get_page(collection_children, per_page=limit, page=(token or False)) # Create dynamic attributes for each page page.next = ( self.pagination_client.insert(keyset=page.paging.bookmark_next) if page.paging.has_next else None ) page.previous = ( self.pagination_client.insert(keyset=page.paging.bookmark_previous) if page.paging.has_previous else None ) except errors.NotFoundError: raise except Exception as e: logger.error(e, exc_info=True) raise errors.DatabaseError( "Unhandled database error when getting collection children" ) links = [] if page.next: links.append( PaginationLink( rel=Relations.next, type="application/geo+json", href=f"{kwargs['request'].base_url}collections/{id}/items?token={page.next}&limit={limit}", method="GET", ) ) if page.previous: links.append( PaginationLink( rel=Relations.previous, type="application/geo+json", href=f"{kwargs['request'].base_url}collections/{id}/items?token={page.previous}&limit={limit}", method="GET", ) ) response_features = [] for item in page: item.base_url = str(kwargs["request"].base_url) response_features.append(schemas.Item.from_orm(item)) context_obj = None if config.settings.api_extension_is_enabled(ApiExtensions.context): context_obj = {"returned": len(page), "limit": limit, "matched": count} return ItemCollection( type="FeatureCollection", context=context_obj, features=response_features, links=links, )