Skip to content

Commit a584c5c

Browse files
authored
Merge pull request #257 from raphaelrpl/b-0.8
Fix breaking package setup due setuptools upgrade
2 parents f1a0f8b + f3c390b commit a584c5c

File tree

10 files changed

+260
-22
lines changed

10 files changed

+260
-22
lines changed

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ Changes
2121
=======
2222

2323

24+
Version 0.8.5 (2023-03-08)
25+
--------------------------
26+
27+
- Fix integration with STAC v1 and STAC Legacy versions
28+
- Add notice in INSTALL for compatibility with package and "setuptools<67"
29+
30+
2431
Version 0.8.4 (2023-01-23)
2532
--------------------------
2633

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ LABEL "org.brazildatacube.description"="Docker image for Data Cube Builder appli
2727
LABEL "org.brazildatacube.git_commit"="${GIT_COMMIT}"
2828

2929
# Build arguments
30-
ARG CUBE_BUILDER_VERSION="0.8.4"
30+
ARG CUBE_BUILDER_VERSION="0.8.5"
3131
ARG CUBE_BUILDER_INSTALL_PATH="/opt/cube-builder/${CUBE_BUILDER_VERSION}"
3232

3333
ADD . ${CUBE_BUILDER_INSTALL_PATH}
3434

3535
WORKDIR ${CUBE_BUILDER_INSTALL_PATH}
3636

37-
RUN python3 -m pip install pip --upgrade setuptools wheel && \
37+
RUN python3 -m pip install pip --upgrade "setuptools<67" wheel && \
3838
python3 -m pip install -e .[rabbitmq] && \
3939
python3 -m pip install gunicorn
4040

INSTALL.rst

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ Installation
2121

2222
The ``Cube Builder`` depends essentially on:
2323

24-
- `Python Client Library for STAC (stac.py) <https://github.com/brazil-data-cube/stac.py>`_
25-
2624
- `Flask <https://palletsprojects.com/p/flask/>`_
2725

2826
- `Celery <http://www.celeryproject.org/>`_
@@ -77,7 +75,7 @@ Install in development mode:
7775

7876
.. code-block:: shell
7977
80-
$ pip3 install -U pip setuptools wheel
78+
$ pip3 install -U pip "setuptools<67" wheel
8179
$ pip3 install -e .[all]
8280
8381
@@ -86,6 +84,13 @@ Install in development mode:
8684
If you have problems with the ``librabbitmq`` installation, please, see [#f1]_.
8785

8886

87+
.. note::
88+
89+
The `setuptools v67+ <https://setuptools.pypa.io/en/latest/history.html>`_ has breaking changes related
90+
Pip versions requirements. For now, you should install ``setuptools<67`` for compatibility.
91+
The packages in ``Cube-Builder`` will be upgraded to support latest version.
92+
93+
8994
Running in Development Mode
9095
---------------------------
9196

@@ -197,12 +202,14 @@ You may need to replace the definition of some parameters:
197202
The command line ``cube-builder worker`` is an auxiliary tool that wraps celery command line
198203
using ``cube_builder`` as context. In this way, all ``celery worker`` parameters are currently supported.
199204
See more in `Celery Workers Guide <https://docs.celeryproject.org/en/stable/userguide/workers.html>`_.
205+
If you keep parameters ``WORK_DIR`` and ``DATA_DIR``, just make sure its writable in order to works, otherwise,
206+
you may see issues related ``Permission Denied``.
200207

201208

202209
.. warning::
203210

204211
The ``Cube Builder`` can use a lot of memory for each concurrent process, since it opens multiple images in memory.
205-
You can limit the concurrent processes in order to prevent it.
212+
You can limit the concurrent processes with ``--concurrency NUMBER`` in order to prevent it.
206213

207214

208215
.. rubric:: Footnotes
@@ -249,4 +256,4 @@ You may need to replace the definition of some parameters:
249256
250257
.. code-block:: shell
251258
252-
$ sudo apt install autoconf
259+
$ sudo apt install autoconf

cube_builder/_adapter.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#
2+
# This file is part of Cube Builder.
3+
# Copyright (C) 2022 INPE.
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
17+
#
18+
19+
"""Define basic module to adapt Python libraries like STAC v1 and legacy versions."""
20+
21+
from abc import ABC, abstractmethod
22+
from copy import deepcopy
23+
from typing import List
24+
from urllib.parse import urljoin
25+
26+
import requests
27+
import shapely.geometry
28+
from pystac_client import Client
29+
from werkzeug.exceptions import abort
30+
31+
32+
class BaseSTAC(ABC):
33+
"""Define base class to represent a STAC interface to communicate with Server."""
34+
35+
uri: str
36+
"""Represent URI for server."""
37+
headers: dict
38+
"""Represent HTTP headers to be attached in requests."""
39+
params: dict
40+
"""Represent HTTP parameters for requests."""
41+
42+
def __init__(self, uri: str, params=None, headers=None, **kwargs):
43+
"""Build STAC signature."""
44+
self.uri = uri
45+
self.params = params
46+
self.headers = headers
47+
self._options = kwargs
48+
49+
@abstractmethod
50+
def search(self, **parameters) -> dict:
51+
"""Search for collection items on STAC server."""
52+
53+
@abstractmethod
54+
def items(self, collection_id: str, **kwargs) -> dict:
55+
"""Access STAC Collection Items."""
56+
57+
@abstractmethod
58+
def collections(self) -> List[dict]:
59+
"""Retrieve the collections from STAC."""
60+
61+
@abstractmethod
62+
def collection(self, collection_id: str) -> dict:
63+
"""Access STAC Collection."""
64+
65+
@staticmethod
66+
def _items_result(features: List[dict], matched: int):
67+
return {
68+
"context": {
69+
"returned": len(features),
70+
"matched": matched
71+
},
72+
"features": features
73+
}
74+
75+
76+
class STACV1(BaseSTAC):
77+
"""Define structure to add support for STAC v1.0+.
78+
79+
This implementation uses `pystac-client <https://pystac-client.readthedocs.io/en/latest/>`_
80+
to communicate with STAC v1.0.
81+
"""
82+
83+
def __init__(self, uri: str, params=None, headers=None, **kwargs):
84+
"""Build STAC instance."""
85+
super(STACV1, self).__init__(uri, params, headers, **kwargs)
86+
87+
self._instance = Client.open(uri, headers=headers, parameters=params, **kwargs)
88+
89+
def search(self, limit=10, max_items=10, **parameters) -> dict:
90+
"""Search for collection items on STAC server."""
91+
max_items = limit
92+
item_search = self._instance.search(limit=limit, max_items=max_items, **parameters)
93+
94+
items = item_search.items()
95+
items = [i.to_dict() for i in items]
96+
97+
return self._items_result(items, matched=item_search.matched())
98+
99+
def collections(self) -> List[dict]:
100+
"""Retrieve the collections from STAC."""
101+
return [c.to_dict() for c in self._instance.get_collections()]
102+
103+
def collection(self, collection_id: str) -> dict:
104+
"""Access STAC Collection."""
105+
collection = self._instance.get_collection(collection_id)
106+
return collection.to_dict()
107+
108+
def items(self, collection_id: str, **kwargs) -> dict:
109+
"""Access STAC Collection Items."""
110+
collection = self._instance.get_collection(collection_id)
111+
112+
items = collection.get_items()
113+
items = [i.to_dict() for i in items]
114+
115+
result = self.search(collections=[collection_id], limit=1, max_items=1)
116+
117+
return self._items_result(items, matched=result['context']['matched'])
118+
119+
120+
class STACLegacy(BaseSTAC):
121+
"""Define structure to add support for legacy versions of STAC server..
122+
123+
This implementation uses `requests.Session <https://requests.readthedocs.io/en/latest/user/advanced/#session-objects>`_
124+
to communicate with STAC legacy versions 0.8x, 0.9x directly.
125+
126+
By default, the ssl entries are ignored. You may override this setting using ``verify=False``.
127+
"""
128+
129+
def __init__(self, uri: str, params=None, headers=None, verify=False, **kwargs):
130+
"""Build STAC instance."""
131+
super(STACLegacy, self).__init__(uri, params, headers, **kwargs)
132+
133+
params = params or {}
134+
headers = headers or {}
135+
136+
self._params = params
137+
self._headers = headers
138+
self._session = requests.session()
139+
self._session.verify = verify
140+
141+
def search(self, **parameters) -> dict:
142+
"""Search for collection items on STAC server."""
143+
options = deepcopy(parameters)
144+
# Remove unsupported values
145+
options.pop('query', None)
146+
url = self._url_resource('search')
147+
148+
try:
149+
response = self._request(url, method='POST', data=options, headers=self._headers, params=self._params)
150+
except:
151+
# Use bbox instead
152+
geom = options.pop('intersects', None)
153+
if geom is None:
154+
raise
155+
156+
options['bbox'] = shapely.geometry.shape(geom).bounds
157+
158+
response = self._request(url, method='POST', data=options, headers=self._headers, params=self._params)
159+
160+
return response
161+
162+
def _request(self, uri: str, method: str = 'GET', data=None, headers=None, params=None):
163+
response = self._session.request(method, uri, headers=headers, params=params, json=data)
164+
if response.status_code != 200:
165+
abort(response.status_code, response.content)
166+
return response.json()
167+
168+
def collections(self) -> List[dict]:
169+
"""Retrieve the collections from STAC."""
170+
uri = self._url_resource('collections')
171+
collections = self._request(uri, params=self._params, headers=self._headers)
172+
return collections
173+
174+
def collection(self, collection_id: str) -> dict:
175+
"""Access STAC Collection."""
176+
uri = self._url_resource(f'collections/{collection_id}')
177+
collection = self._request(uri, params=self._params, headers=self._headers)
178+
return collection
179+
180+
def items(self, collection_id: str, **kwargs) -> dict:
181+
"""Access STAC Collection Items."""
182+
return self.search(collections=[collection_id], limit=1)
183+
184+
def _url_resource(self, resource: str) -> str:
185+
return urljoin(self.uri + '/', resource)
186+
187+
188+
def build_stac(uri, headers=None, **parameters) -> BaseSTAC:
189+
"""Build a STAC instance according versions."""
190+
response = requests.get(uri, timeout=15, headers=headers, params=parameters)
191+
192+
response.raise_for_status()
193+
194+
catalog = response.json()
195+
if not catalog.get('stac_version'):
196+
raise RuntimeError(f'Invalid STAC "{uri}", missing "stac_version"')
197+
198+
stac_version = catalog['stac_version']
199+
if stac_version.startswith('0.'):
200+
return STACLegacy(uri, params=parameters, headers=headers)
201+
return STACV1(uri, params=parameters, headers=headers)

cube_builder/celery/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"""Define Cube Builder celery module initialization."""
2020

2121
import logging
22+
import os
2223

2324
import flask
2425
from bdc_catalog.models import db
@@ -27,6 +28,7 @@
2728
from flask import Flask
2829

2930
from cube_builder.config import Config
31+
from ..constants import to_bool
3032

3133
CELERY_TASKS = [
3234
'cube_builder.celery.tasks',
@@ -59,6 +61,7 @@ def create_celery_app(flask_app: Flask) -> Celery:
5961

6062
always_eager = flask_app.config.get('TESTING', False)
6163
celery.conf.update(dict(
64+
CELERY_ACKS_LATE=to_bool(os.getenv('CELERY_ACKS_LATE', '1')),
6265
CELERY_TASK_ALWAYS_EAGER=always_eager,
6366
CELERYD_PREFETCH_MULTIPLIER=Config.CELERYD_PREFETCH_MULTIPLIER,
6467
CELERY_RESULT_BACKEND='db+{}'.format(flask_app.config.get('SQLALCHEMY_DATABASE_URI')),

cube_builder/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
"""Brazil Data Cube Configuration."""
1919

2020
import os
21-
from distutils.util import strtobool
2221

2322
from .version import __version__
23+
from .constants import to_bool
2424

2525
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
2626

@@ -99,7 +99,7 @@ class Config:
9999
BDC_AUTH_ACCESS_TOKEN_URL = os.getenv('BDC_AUTH_ACCESS_TOKEN_URL', None)
100100
"""Access token url used for retrieving user info in BDC-Auth
101101
Defaults to ``None``. Used when ``BDC_AUTH_REQUIRED`` is set."""
102-
BDC_AUTH_REQUIRED = strtobool(os.getenv('BDC_AUTH_REQUIRED', '0'))
102+
BDC_AUTH_REQUIRED = to_bool(os.getenv('BDC_AUTH_REQUIRED', '0'))
103103
"""Flag to manage when a Auth is required.
104104
Defaults to ``0``, that means that there is not authorization request to access ``Cube Builder`` API."""
105105

cube_builder/constants.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,25 @@
7777
PNG_MIME_TYPE = 'image/png'
7878

7979
SRID_ALBERS_EQUAL_AREA = 100001
80+
81+
82+
def to_bool(val: str):
83+
"""Convert a string representation to true or false.
84+
85+
This method was adapted from `pypa/distutils <https://github.com/pypa/distutils>`_
86+
to avoid import deprecated module.
87+
88+
The following values are supported:
89+
- ``True``: 'y', 'yes', 't', 'true', 'on', and '1'
90+
- ``False``: 'n', 'no', 'f', 'false', 'off', and '0'
91+
92+
Raises:
93+
ValueError: When the given string value could not be converted to boolean.
94+
"""
95+
val = val.lower()
96+
if val in ('y', 'yes', 't', 'true', 'on', '1',):
97+
return 1
98+
elif val in ('n', 'no', 'f', 'false', 'off', '0',):
99+
return 0
100+
101+
raise ValueError(f"invalid boolean value for {val}")

0 commit comments

Comments
 (0)