Skip to content

IO updates, new StacApiIO class #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 60 commits into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
9983673
linting
matthewhanson Jun 2, 2021
bca15d2
initial StacIO updates
matthewhanson Jun 3, 2021
cfe7e1a
StacApiIO updates
matthewhanson Jun 5, 2021
accf058
remove extensions implementation that were based on old PySTAC extens…
matthewhanson Jun 5, 2021
e3364a5
updated changelog
matthewhanson Jun 6, 2021
c9d5ba4
updated CHANGELOG
matthewhanson Jun 6, 2021
de16bab
update cli to use search.get_all_items()
matthewhanson Jun 6, 2021
242699d
refactor IO, create StacApiIO
matthewhanson Jun 6, 2021
7b6f72c
bump to beta version
matthewhanson Jun 6, 2021
2d7dbbf
updated ItemCollection class
matthewhanson Jun 6, 2021
d3fb58d
refactor client and item_search to use new StacApiIO class
matthewhanson Jun 6, 2021
a14cbe5
Merge branch 'main' of https://github.com/stac-utils/pystac-api-clien…
matthewhanson Jun 6, 2021
0158d05
update tests
matthewhanson Jun 7, 2021
c63aa08
allow ItemSearch to be used without specifying stac_io class (will cr…
matthewhanson Jun 7, 2021
3ba2785
misc test updates
matthewhanson Jun 7, 2021
d4f2758
add ParametersError class
matthewhanson Jun 8, 2021
17133d4
add conforms_to function to Client
matthewhanson Jun 12, 2021
0e90618
fix matched function when providing limit
matthewhanson Jun 12, 2021
1a91137
use new PySTAC ItemCollection class
matthewhanson Jun 12, 2021
5732071
refactor conformance to use single base class to manage supported API…
matthewhanson Jun 15, 2021
1d1866d
update CHANGELOG
matthewhanson Jun 15, 2021
e6eafe8
remove old stac api object class
matthewhanson Jun 15, 2021
4222ef9
removed read_json, fixed upstream in PySTAC
matthewhanson Jun 15, 2021
cf7a8fc
return True from check_conformance
matthewhanson Jun 15, 2021
701610f
update client tests
matthewhanson Jun 15, 2021
448266e
add list of STAC_URLS for testing
matthewhanson Jun 15, 2021
b319c82
update tests with STAC_URLS array
matthewhanson Jun 15, 2021
019baf2
add planetary computer root JSON
matthewhanson Jun 15, 2021
7c431f9
new test file
matthewhanson Jun 15, 2021
a9304cd
updated VCR cassettes (for testing)
matthewhanson Jun 16, 2021
737282c
remove passing in Collection objects to ItemSearch
matthewhanson Jun 16, 2021
31ab075
set limit to min of limit or max-items
matthewhanson Jun 16, 2021
a3f300b
updated tests and cassettes
matthewhanson Jun 16, 2021
ade115c
linting updates
matthewhanson Jun 16, 2021
d7fb3ba
code formatting
matthewhanson Jun 16, 2021
13be883
peg pystac dependency to a commit
matthewhanson Jun 16, 2021
7c6cd62
pystac http repo link in setup
matthewhanson Jun 16, 2021
b805334
peg pystac to 1.0.0rc.1
matthewhanson Jun 18, 2021
25a5886
do not convert items to ItemCollection in CLI before saving
matthewhanson Jun 18, 2021
27dbc0d
remove client.from_file, fixed upstream in PySTAC
matthewhanson Jun 19, 2021
8b9ad6b
use StacApiIO in CLI
matthewhanson Jun 19, 2021
cbece20
Update pystac_client/conformance.py
matthewhanson Jun 19, 2021
40d8654
simplify logger propagation
matthewhanson Jun 19, 2021
3192c8a
remove commented code
matthewhanson Jun 19, 2021
1eb6d49
Merge branch 'mah/updates' of github.com:stac-utils/pystac-client int…
matthewhanson Jun 19, 2021
b7ed654
Update conforms_to docstring
matthewhanson Jun 19, 2021
a10557b
upate type for conformance
matthewhanson Jun 19, 2021
4c13be2
Merge branch 'mah/updates' of github.com:stac-utils/pystac-client int…
matthewhanson Jun 19, 2021
af9ef01
raise error conformance class does not exist
matthewhanson Jun 19, 2021
45cbf47
terseness
matthewhanson Jun 19, 2021
237cf0a
Merge branch 'mah/updates' of github.com:stac-utils/pystac-client int…
matthewhanson Jun 19, 2021
1a591de
fix indent
matthewhanson Jun 20, 2021
88a0767
pass in empty string to _json_loads
matthewhanson Jun 21, 2021
231e9e2
update from_dict in Client
matthewhanson Jun 21, 2021
e7bc770
revert to original saving/output of item collection in CLI
matthewhanson Jun 21, 2021
07bc110
formatting updates
matthewhanson Jun 23, 2021
e114ee1
add ConformanceClass enum
matthewhanson Jun 23, 2021
8e398c3
use warnings.warn for warnings
matthewhanson Jun 23, 2021
fafd842
update tests to remove deprecated functions
matthewhanson Jun 23, 2021
124a0e6
linting updates
matthewhanson Jun 23, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,39 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added

- `Client.open` falls back to the `STAC_URL` environment variable if no url is provided as an argument [#48](https://github.com/stac-utils/pystac-client/pull/48)
- New Search.get_pages() iterator function to retrieve pages as raw JSON, not as ItemCollections
- `StacApiIO` class added, subclass from PySTAC `StacIO`. A `StacApiIO` instance is used for all IO for a Client instance, and all requests
are in a single HTTP session.
- `conformance.CONFORMANCE_CLASSES` dictionary added containing all STAC API Capabilities from stac-api-spec

### Changed

- IO changed to use PySTAC's new StacIO base class.
- `Search.item_collections()` renamed to `Search.get_item_collections()`
- `Search.item_collections()` renamed to `Search.get_items()`
- Conformance is checked by each individual function that requires a particular conformance
- STAC API testing URLs changed to updated APIs

### Fixed

- Running `stac-client` with no arguments no longer raises a confusing exception [#52](https://github.com/stac-utils/pystac-client/pull/52)
- `Client.get_collections_list` [#44](https://github.com/stac-utils/pystac-client/issues/44)
- The regular expression used for datetime parsing [#59](https://github.com/stac-utils/pystac-client/pull/59)

### Removed

- `get_pages` and `simple_stac_resolver` functions from `pystac_client.stac_io` (The new StacApiIO class understands `Link` objects)
- `Client.search()` no longer accepts a `next_resolver` argument
- pystac.extensions modules, which were based on PySTAC's previous extension implementation, replaced in 1.0.0
- `stac_api_object.StacApiObjectMixin`, replaced with `conformance.ConformanceMixin`
- `conformance.ConformanceClass`, replaced with `conformance.ConformanceMixin`
- `conformance.ConformanceClasses`, replaced with `conformance.ConformanceMixin`
- PySTAC Collection objects can no longer be passed in as `collections` arguments to the `ItemSearch` class (just pass ids)

### Deprecated
- `Search.item_collections()`
- `Search.items()`

## [v0.1.1] - 2021-04-16

### Added
Expand Down
18 changes: 0 additions & 18 deletions pystac_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
# flake8: noqa
from pystac import STAC_IO
import pystac.extensions.base

from pystac_client.version import __version__
from pystac_client.stac_api_object import STACAPIObjectMixin
from pystac_client.item_collection import ItemCollection
from pystac_client.extensions import APIExtensions
from pystac_client.item_search import ItemSearch
from pystac_client.client import Client
from pystac_client.conformance import ConformanceClasses

from pystac_client.stac_io import read_text_method

from pystac_client import extensions
import pystac_client.extensions.context

# Replace the read_text_method
STAC_IO.read_text_method = read_text_method

# Add API Extensions
STAC_API_EXTENSIONS = pystac.extensions.base.RegisteredSTACExtensions(
[extensions.context.CONTEXT_EXTENSION_DEFINITION])
10 changes: 4 additions & 6 deletions pystac_client/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
import sys

from .item_collection import ItemCollection
from .client import Client
from .version import __version__

Expand All @@ -24,12 +23,12 @@ def search(url=STAC_URL, matched=False, save=None, headers=None, **kwargs):
matched = search.matched()
print('%s items matched' % matched)
else:
items = ItemCollection(search.items())
feature_collection = search.get_all_items_as_dict()
if save:
with open(save, 'w') as f:
f.write(json.dumps(items.to_dict()))
f.write(json.dumps(feature_collection))
else:
print(json.dumps(items.to_dict()))
print(json.dumps(feature_collection))

except Exception as e:
logger.error(e, exc_info=True)
Expand Down Expand Up @@ -134,8 +133,7 @@ def cli():
if args.get('save', False) or args.get('matched', False):
logging.basicConfig(stream=sys.stdout, level=loglevel)
# quiet loggers
for lg in ['urllib3']:
logging.getLogger(lg).propagate = False
logging.getLogger("urllib3").propagate = False

if args.get('url', None) is None:
raise RuntimeError('No STAC URL provided')
Expand Down
147 changes: 51 additions & 96 deletions pystac_client/client.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
from copy import deepcopy
import os
from typing import Callable, Optional
from urllib.request import Request
from typing import Any, Dict, List, Optional

import pystac
import pystac.stac_object
from pystac.link import Link
from pystac.errors import STACTypeError
import pystac.validation
from pystac import STAC_IO

from pystac_client.conformance import ConformanceClasses
from pystac_client.exceptions import ConformanceError
from pystac_client.item_search import (
BBoxLike,
CollectionsLike,
DatetimeLike,
IDsLike,
IntersectsLike,
QueryLike,
ItemSearch,
)
from pystac_client.stac_api_object import STACAPIObjectMixin


class Client(pystac.Catalog, STACAPIObjectMixin):

from pystac_client.conformance import ConformanceClasses, ConformanceMixin
from pystac_client.item_search import (BBoxLike, CollectionsLike, DatetimeLike, IDsLike,
IntersectsLike, QueryLike, ItemSearch)
from pystac.serialization import (identify_stac_object, migrate_to_latest)
from pystac_client.stac_io import StacApiIO


class Client(pystac.Catalog, ConformanceMixin):
"""Instances of the ``Client`` class inherit from :class:`pystac.Catalog` and provide a convenient way of interacting
with Catalogs OR APIs that conform to the `STAC API spec <https://github.com/radiantearth/stac-api-spec>`_. In addition
to being a valid `STAC Catalog <https://github.com/radiantearth/stac-spec/blob/master/catalog-spec/catalog-spec.md>`_ the
Expand All @@ -44,13 +36,13 @@ class Client(pystac.Catalog, STACAPIObjectMixin):
<http://docs.opengeospatial.org/is/17-069r3/17-069r3.html#_declaration_of_conformance_classes>`_.
"""
def __init__(self,
id,
description,
title=None,
stac_extensions=None,
extra_fields=None,
href=None,
catalog_type=None,
id: str,
description: str,
title: Optional[str] = None,
stac_extensions: Optional[List[str]] = None,
extra_fields: Optional[Dict[str, Any]] = None,
href: Optional[str] = None,
catalog_type: pystac.CatalogType = pystac.CatalogType.ABSOLUTE_PUBLISHED,
conformance=None,
headers=None):
super().__init__(id=id,
Expand All @@ -62,18 +54,11 @@ def __init__(self,
catalog_type=catalog_type)

self.conformance = conformance

# Check that the API conforms to the STAC API - Core spec (or ignore if None)
if conformance is not None and not self.conforms_to(ConformanceClasses.STAC_API_CORE):
allowed_uris = "\n\t".join(ConformanceClasses.STAC_API_CORE.all_uris)
raise ConformanceError(
'API does not conform to {ConformanceClasses.STAC_API_CORE}. Must contain one of the following '
f'URIs to conform (preferably the first):\n\t{allowed_uris}.')

self.conforms_to(ConformanceClasses.CORE)
self.headers = headers or {}

def __repr__(self):
return '<Catalog id={}>'.format(self.id)
return '<Client id={}>'.format(self.id)

@classmethod
def open(cls, url=None, headers=None):
Expand All @@ -88,7 +73,6 @@ def open(cls, url=None, headers=None):
-------
catalog : Client
"""
import pystac_client.stac_io

if url is None:
url = os.environ.get("STAC_URL")
Expand All @@ -97,26 +81,18 @@ def open(cls, url=None, headers=None):
raise TypeError(
"'url' must be specified or the 'STAC_URL' environment variable must be set.")

def read_text_method(url):
request = Request(url, headers=headers or {})
return pystac_client.stac_io.read_text_method(request)

old_read_text_method = STAC_IO.read_text_method
STAC_IO.read_text_method = read_text_method
try:
catalog = cls.from_file(url)
finally:
STAC_IO.read_text_method = old_read_text_method
catalog.headers = headers
stac_io = StacApiIO(headers=headers)

catalog = cls.from_file(url, stac_io)
return catalog

@classmethod
def from_dict(
cls,
d,
href=None,
root=None,
):
def from_dict(cls,
d: Dict[str, Any],
href: Optional[str] = None,
migrate: bool = False,
preserve_dict: bool = True,
**kwargs: Any) -> "Client":
"""Overwrites the :meth:`pystac.Catalog.from_dict` method to add the ``user_agent`` initialization argument
and to check if the content conforms to the STAC API - Core spec.

Expand All @@ -127,40 +103,33 @@ def from_dict(
response or in a ``/conformance``. According to the STAC API - Core spec, services must publish this as
part of a ``"conformsTo"`` attribute, but some legacy APIs fail to do so.
"""
if migrate:
info = identify_stac_object(d)
d = migrate_to_latest(d, info)

if not cls.matches_object_type(d):
raise STACTypeError(f"{d} does not represent a {cls.__name__} instance")

catalog_type = pystac.CatalogType.determine_type(d)

d = deepcopy(d)
if preserve_dict:
d = deepcopy(d)

id = d.pop('id')
description = d.pop('description')
title = d.pop('title', None)
stac_extensions = d.pop('stac_extensions', None)
links = d.pop('links')
# allow for no conformance, for now
conformance = d.pop('conformsTo', None)
d.pop("stac_version")
links = d.pop("links")

d.pop('stac_version')

catalog = cls(
id=id,
description=description,
title=title,
stac_extensions=stac_extensions,
conformance=conformance,
extra_fields=d,
href=href,
catalog_type=catalog_type,
)
client = cls(**d, conformance=conformance, catalog_type=catalog_type, **kwargs)

for link in links:
if link['rel'] == 'root':
if link["rel"] == pystac.RelType.ROOT:
# Remove the link that's generated in Catalog's constructor.
catalog.remove_links('root')
client.remove_links(pystac.RelType.ROOT)

if link['rel'] != 'self' or href is None:
catalog.add_link(pystac.Link.from_dict(link))
if link["rel"] != pystac.RelType.SELF or href is None:
client.add_link(Link.from_dict(link))

return catalog
return client

def get_collections_list(self):
"""Gets list of available collections from this Catalog. Alias for get_child_links since children
Expand All @@ -178,8 +147,7 @@ def search(self,
collections: Optional[CollectionsLike] = None,
query: Optional[QueryLike] = None,
max_items: Optional[int] = None,
method: Optional[str] = 'POST',
next_resolver: Optional[Callable] = None) -> ItemSearch:
method: Optional[str] = 'POST') -> ItemSearch:
"""Query the ``/search`` endpoint using the given parameters.

This method returns an :class:`~pystac_client.ItemSearch` instance, see that class's documentation
Expand Down Expand Up @@ -240,9 +208,6 @@ def search(self,
``None``. If ``None``, this will default to ``"POST"`` if the ``intersects`` argument is present and
``"GET"`` if not. If a ``"POST"`` request receives a ``405`` status for the response, it will automatically
retry with a ``"GET"`` request for all subsequent requests.
next_resolver: Callable, optional
A callable that will be used to construct the next request based on a "next" link and the previous request.
Defaults to using the :func:`~pystac_client.paging.simple_stac_resolver`.

Returns
-------
Expand All @@ -255,22 +220,14 @@ def search(self,
<https://github.com/radiantearth/stac-api-spec/tree/master/item-search>`__ or does not have a link with
a ``"rel"`` type of ``"search"``.
"""
if self.conformance is not None and not self.conforms_to(
ConformanceClasses.STAC_API_ITEM_SEARCH):
spec_name = ConformanceClasses.STAC_API_ITEM_SEARCH.name
spec_uris = '\n\t'.join(ConformanceClasses.STAC_API_ITEM_SEARCH.all_uris)
msg = f'This service does not conform to the {spec_name} spec and therefore the search method is not ' \
f'implemented. Services must publish one of the following conformance URIs in order to conform to ' \
f'this spec (preferably the first one):\n\t{spec_uris}'
raise NotImplementedError(msg)

search_link = self.get_single_link('search')
if search_link is None:
raise NotImplementedError(
'No link with a "rel" type of "search" could be found in this services\'s '
'root catalog.')
'No link with "rel" type of "search" could be found in this catalog')

return ItemSearch(search_link.target,
conformance=self.conformance,
limit=limit,
bbox=bbox,
datetime=datetime,
Expand All @@ -280,6 +237,4 @@ def search(self,
query=query,
max_items=max_items,
method=method,
headers=self.headers,
conformance=self.conformance,
next_resolver=next_resolver)
stac_io=self._stac_io)
Loading