diff --git a/examples/describe_collection.py b/examples/describe_collection.py index dd90deb..65732e4 100644 --- a/examples/describe_collection.py +++ b/examples/describe_collection.py @@ -10,7 +10,7 @@ from wlts import WLTS # Specify the URL of the WLTS instance to be used -service = WLTS('https://brazildatacube.dpi.inpe.br/wlts/') +service = WLTS('https://data.inpe.br/bdc/wlts/v1') # Get collection metadata print(service['prodes_amazonia_legal']) diff --git a/examples/list_collection.py b/examples/list_collection.py index 8dd60ec..44faa5b 100644 --- a/examples/list_collection.py +++ b/examples/list_collection.py @@ -11,7 +11,7 @@ # You should create a wlts object attached to a given service # Specify the URL of the WLTS instance to be used -service = WLTS('https://brazildatacube.dpi.inpe.br/wlts/') +service = WLTS('https://data.inpe.br/bdc/wlts/v1/') # Returns the list of collections available on the service print(service.collections) diff --git a/examples/text-repr.py b/examples/text-repr.py index 852ab6d..f587a15 100644 --- a/examples/text-repr.py +++ b/examples/text-repr.py @@ -9,7 +9,7 @@ from wlts import * -service = WLTS('https://brazildatacube.dpi.inpe.br/wlts/') +service = WLTS('https://data.inpe.br/bdc/wlts/v1/') print(service) print(str(service)) diff --git a/examples/trajectory.py b/examples/trajectory.py index 9b28d29..01bb058 100644 --- a/examples/trajectory.py +++ b/examples/trajectory.py @@ -9,7 +9,7 @@ from wlts import WLTS # Specify the URL of the WLTS instance to be used -service = WLTS(url='https://brazildatacube.dpi.inpe.br/wlts/', access_token='change-me') +service = WLTS(url='https://data.inpe.br/bdc/wlts/v1/') # Example of trajectory operation # Make sure the collection is available in service @@ -31,6 +31,7 @@ collections='prodes_amazonia_legal,mapbiomas-v8', start_date='2010' ) +print (tj_m) for tj in tj_m['trajectories']: print(tj.trajectory) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9856eb5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,74 @@ +[project] +name = "wlts" +description = "." +readme = "README.rst" +requires-python = ">=3.8" +license = {file = "LICENSE"} +authors = [ + {name = "Brazil Data Cube Team", email = "bdc.team@inpe.br"}, +] +keywords = [ + "lulc" +] +classifiers = [ + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU General Public License version 3 License", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", +] +version="1.2.0" +dependencies = [ + "Click>=7.0", + "Jinja2>=2.11.1", + "descartes>=1.1.0", + "shapely>=1.7.1", + "pandas>=1.1", + "geopandas>=0.8.2", + "plotly==5.5.0", + "rich>=13.9.2", + "lccs @ git+https://github.com/brazil-data-cube/lccs.py@b-1.0", + "rich>=10.0.0", + "httpx>=0.19.0", +] + +# Extras Dependencies +[project.optional-dependencies] +dev = ["pre-commit"] +docs = [ + "Sphinx>=7.0", + "sphinx_rtd_theme", + "sphinx-copybutton", + "sphinx-tabs", +] +tests = [ + "coverage>=6.4", + "coveralls>=3.3", + "pytest>=7.4", + "pytest-cov>=4.1", + "pytest-pep8>=1.0", + "pydocstyle>=4.0", + "isort>4.3", + "check-manifest>=0.40", +] +all = ["wlts[docs,tests]"] +## End extras dependencies + +[build-system] +requires = ["setuptools>=67.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["wlts*"] +exclude = ["tests*"] +namespaces = false + +[tool.setuptools.package-data] +"wlts" = ["py.typed"] + +[project.scripts] +wlts-cli = "wlts.cli:cli" diff --git a/run-tests.sh b/run-tests.sh index 3bd5577..1880d7d 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -20,5 +20,5 @@ pydocstyle wlts examples tests setup.py && \ isort wlts examples tests setup.py --check-only --diff && \ check-manifest --ignore ".drone.yml,.readthedocs.yml" && \ -sphinx-build -qnW --color -b doctest docs/sphinx/ docs/sphinx/_build/doctest && \ +sphinx-build -qn --color -b doctest docs/sphinx/ docs/sphinx/_build/doctest && \ pytest diff --git a/setup.py b/setup.py index 7a2de81..28ef42d 100644 --- a/setup.py +++ b/setup.py @@ -18,97 +18,6 @@ """Python Client Library for the Web Land Trajectory Service.""" -import os +from setuptools import setup -from setuptools import find_packages, setup - -readme = open('README.rst').read() - -history = open('CHANGES.rst').read() - -docs_require = [ - 'Sphinx>=2.2', - 'sphinx_rtd_theme', - 'sphinx-copybutton', -] - -tests_require = [ - 'coverage>=4.5', - 'pytest>=5.2', - 'pytest-cov>=2.8', - 'requests-mock[fixture]', - 'pytest-pep8>=1.0', - 'pydocstyle>=4.0', - 'isort>4.3', - 'check-manifest>=0.40', - 'requests-mock>=1.7.0' -] - -extras_require = { - 'docs': docs_require, - 'tests': tests_require, -} - -extras_require['all'] = [ req for exts, reqs in extras_require.items() for req in reqs ] - -setup_requires = [ - 'pytest-runner>=5.2', -] - -install_requires = [ - 'requests>=2.20', - 'Click>=7.0', - 'Jinja2>=2.11.1', - 'descartes>=1.1.0', - 'shapely>=1.7.1', - 'pandas>=1.1', - 'geopandas>=0.8.2', - 'plotly==5.5.0', - 'lccs @ git+https://github.com/brazil-data-cube/lccs.py@v0.9.0', -] - -packages = find_packages() - -with open(os.path.join('wlts', 'version.py'), 'rt') as fp: - g = {} - exec(fp.read(), g) - version = g['__version__'] - -setup( - name='wlts', - version=version, - description=__doc__, - long_description=readme + '\n\n' + history, - keywords=['Land Use Land Cover', 'GIS', 'Web Services', 'OGC WFS', 'OGC WCS', 'Web Time Series Service'], - license='GPLv3', - author='Brazil Data Cube Team', - author_email='brazildatacube@inpe.br', - url='https://github.com/brazil-data-cube/wlts.py', - packages=packages, - zip_safe=False, - include_package_data=True, - platforms='any', - entry_points={ - 'console_scripts': [ - 'wlts-cli = wlts.cli:cli', - ], - }, - extras_require=extras_require, - install_requires=install_requires, - setup_requires=setup_requires, - tests_require=tests_require, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Education', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Topic :: Scientific/Engineering :: GIS', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], -) +setup() diff --git a/tests/test_trajectory.py b/tests/test_trajectory.py index e4e2e84..d75b8c9 100644 --- a/tests/test_trajectory.py +++ b/tests/test_trajectory.py @@ -163,7 +163,7 @@ def test_trajectory(self, wlts_objects, requests_mock, runner, config_obj): ], obj=config_obj) assert result.exit_code == 0 - assert 'trajectory:' in result.output + assert 'Processing trajectory request...' in result.output if __name__ == '__main__': diff --git a/wlts/cli.py b/wlts/cli.py index 2039e97..61232a0 100644 --- a/wlts/cli.py +++ b/wlts/cli.py @@ -17,8 +17,16 @@ # """Command line interface for the WLTS client.""" +from time import time import click +from rich.console import Console +from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn +from rich.syntax import Syntax +from rich.table import Table +from rich.panel import Panel +from rich.tree import Tree + from .wlts import WLTS @@ -35,13 +43,25 @@ def __init__(self): pass_config = click.make_pass_decorator(Config, ensure=True) +console = Console() + @click.group() -@click.option('--url', type=click.STRING, default='https://brazildatacube.dpi.inpe.br/wlts/', - help='The WLTS server address (an URL).') -@click.option('--lccs-url', type=click.STRING, default='https://brazildatacube.dpi.inpe.br/lccs', - help='The LCCS-WS address (an URL).') -@click.option('--access-token', default=None, help='Personal Access Token of the BDC Auth') +@click.option( + "--url", + type=click.STRING, + default="https://data.inpe.br/bdc/wlts/v1/", + help="The WLTS server address (an URL).", +) +@click.option( + "--lccs-url", + type=click.STRING, + default="https://brazildatacube.dpi.inpe.br/lccs", + help="The LCCS-WS address (an URL).", +) +@click.option( + "--access-token", default=None, help="Personal Access Token of the BDC Auth" +) @click.version_option() @pass_config def cli(config, url, lccs_url, access_token): @@ -51,72 +71,118 @@ def cli(config, url, lccs_url, access_token): @cli.command() -@click.option('-v', '--verbose', is_flag=True, default=False) +@click.option("-v", "--verbose", is_flag=True, default=False) @pass_config def list_collections(config: Config, verbose): """Return the list of available collections in the service provider.""" if verbose: - click.secho(f'Server: {config.url}', bold=True, fg='black') - click.secho('\tRetrieving the list of available coverages... ', - bold=False, fg='black') + console.print(f"[bold black]Server: [green]{config.url}[/green]", style="bold") + console.print( + "[black]\tRetrieving the list of available collections...[/black]" + ) + + table = Table( + title="Available Collections", show_header=True, header_style="bold magenta" + ) + table.add_column("Collection Name", style="green", no_wrap=True) + table.add_column("Collection Title", style="green", no_wrap=True) + for collection in config.service.collections: - click.secho(f'\t\t- {collection}', bold=True, fg='green') + describe_collection = config.service[collection] + table.add_row(collection, describe_collection["title"]) - click.secho('\tFinished!', bold=False, fg='black') + console.print(table) + console.print("[black]\tFinished![/black]") else: - for cv in config.service.collections: - click.secho(f'{cv}', bold=True, fg='green') + for collection in config.service.collections: + console.print(f"[green]{collection}[/green]", style="bold") @cli.command() -@click.option('-v', '--verbose', is_flag=True, default=False) -@click.option('-c', '--collection', required=True, type=str, - help='The collection name') +@click.option("-v", "--verbose", is_flag=True, default=False) +@click.option("-c", "--collection", required=True, type=str, help="The collection name") @pass_config def describe(config: Config, verbose, collection): - """Retrieve the coverage metadata.""" + """Retrieve the collection metadata.""" + # Retrieve the collection metadata + cv = config.service[collection] + if verbose: - click.secho(f'Server: {config.url}', bold=True, fg='black') - click.secho('\tRetrieving the collection metadata... ', - bold=False, fg='black') + console.print(f"[bold black]Server: [green]{config.url}[/green]", style="bold") + console.print("[black]\tRetrieving the collection metadata...[/black]") - cv = config.service[collection] + tree = Tree(cv["title"], guide_style="bold cyan") - click.secho(f'\t- {cv}', bold=True, fg='green') - if verbose: - click.secho('\tFinished!', bold=False, fg='black') + tree.add(f"[bold green]ID[/bold green]: {cv['classification_system']['id']}") + tree.add(f"[bold green]Name[/bold green]: {cv['classification_system']['name']}") + tree.add(f"[bold green]Title[/bold green]: {cv['classification_system']['title']}") + tree.add(f"[bold green]Version[/bold green]: {cv['classification_system']['version']}") + tree.add(f"[bold green]Type[/bold green]: {cv['classification_system']['type']}") + + table = Table(title="Overview", expand=True) + table.add_column("Key", justify="right", style="cyan", no_wrap=True) + table.add_column("Value", style="magenta") + + table.add_row("Collection Type", cv["collection_type"]) + table.add_row("Description", cv["description"]) + table.add_row("Period", f"{cv['period']['start_date']} a {cv['period']['end_date']}") + table.add_row("Spatial Extent", + f"Xmin: {cv['spatial_extent']['xmin']}, Xmax: {cv['spatial_extent']['xmax']}, Ymin: {cv['spatial_extent']['ymin']}, Ymax: {cv['spatial_extent']['ymax']}") + table.add_row("Temporal Resolution", + f"{cv['temporal_resolution']['value']} {cv['temporal_resolution']['unit']}") + + console.print(Panel(tree, title="Classification System")) + console.print(table) + + console.print("[black]\tFinished![/black]") + + else: + import json + # Convert the metadata to a formatted JSON string + formatted_json = json.dumps(cv, indent=4, ensure_ascii=False) + + # Use Syntax from rich to display JSON nicely formatted + syntax = Syntax(formatted_json, "json", theme="monokai", line_numbers=True) + console.print(f"\t[green bold]- Collection Metadata:[/green bold]") + console.print(syntax) # Pretty formatted JSON with syntax highlighting @cli.command() -@click.option('-v', '--verbose', is_flag=True, default=False) -@click.option('-a', '--collections', required=False, type=str, - help='Collections list (items separated by comma)') -@click.option('--latitude', required=True, type=float, - help='Latitude in EPSG:4326') -@click.option('--longitude', required=True, type=float, - help='Longitude in EPSG:4326') -@click.option('--start-date', required=False, default=None, type=str, - help='Start date') -@click.option('--end-date', required=False, default=None, type=str, - help='End date') -@click.option('--start-date', required=False, default=None, type=str, - help='Start date') -@click.option('--end-date', required=False, default=None, type=str, - help='End date') -@click.option('--language', required=False, default=None, type=str, - help='Language') +@click.option("-v", "--verbose", is_flag=True, default=False) +@click.option( + "-a", + "--collections", + required=False, + type=str, + help="Collections list (items separated by comma)", +) +@click.option("--latitude", required=True, type=float, help="Latitude in EPSG:4326") +@click.option("--longitude", required=True, type=float, help="Longitude in EPSG:4326") +@click.option("--start-date", required=False, default=None, type=str, help="Start date") +@click.option("--end-date", required=False, default=None, type=str, help="End date") +@click.option("--start-date", required=False, default=None, type=str, help="Start date") +@click.option("--end-date", required=False, default=None, type=str, help="End date") +@click.option("--language", required=False, default=None, type=str, help="Language") @pass_config -def trajectory(config: Config, verbose, collections, start_date, end_date, latitude, longitude, language): +def trajectory( + config: Config, + verbose, + collections, + start_date, + end_date, + latitude, + longitude, + language, +): """Return the trajectory associated to the location.""" if verbose: - click.secho(f'Server: {config.url}', bold=True, fg='black') - click.secho('\tRetrieving trajectory... ', - bold=False, fg='black') + console.print(f"[bold black]Server: [green]{config.url}[/green]") + console.print("[black]\tRetrieving trajectory...[/black]") + # Prepare query parameters args = dict() - if collections: args["collections"] = collections if start_date: @@ -126,13 +192,47 @@ def trajectory(config: Config, verbose, collections, start_date, end_date, latit if language: args["language"] = language - retval = config.service.tj( - latitude=latitude, - longitude=longitude, - **args - ) + # Progress bar to indicate processing time + with Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TimeElapsedColumn(), + ) as progress: + task = progress.add_task("[cyan]Processing trajectory request...", total=100) + + # Measure start time + start_time = time() + + # Simulate progress + for _ in range(5): + progress.update(task, advance=20) + + # Retrieve trajectory data + retval = config.service.tj(latitude=latitude, longitude=longitude, **args) + + # Measure end time + end_time = time() + + # Calculate total time + total_time = end_time - start_time + + # Display the trajectory data in a table format + table = Table(title=f"Trajectory Results (Time: {total_time:.2f} seconds)") + + # Add table columns + table.add_column("Class", style="cyan", no_wrap=True) + table.add_column("Collection", style="magenta") + table.add_column("Date", justify="right", style="green") + table.add_column("Point ID", justify="right", style="yellow") + + # Add rows from the trajectory data + for entry in retval.trajectory: + table.add_row( + entry["class"], entry["collection"], entry["date"], str(entry["point_id"]) + ) - click.secho(f'\ttrajectory: {retval.trajectory}') + # Display the table + console.print(table) if verbose: - click.secho('\tFinished!', bold=False, fg='black') + console.print(f"[black]\tFinished in {total_time:.2f} seconds![/black]") diff --git a/wlts/collection.py b/wlts/collection.py index 0b8526f..9837a90 100644 --- a/wlts/collection.py +++ b/wlts/collection.py @@ -16,86 +16,82 @@ # along with this program. If not, see . # """A class that represents a Collection in WLTS.""" +from typing import Any, Dict, Union + from .utils import Utils class Collections(dict): """A class that describes a collection in WLTS. - .. note:: - For more information about collection definition, please, refer to - `WLTS specification `_. + For more information about collection definition, refer to + `WLTS specification `_. """ - def __init__(self, service, metadata=None): + def __init__(self, service: Any, metadata: Dict[str, Any] = None) -> None: """Create a collection object associated to a WLTS client. Args: service (wlts.WLTS): The client to be used by the collection object. - metadata (dict): The collection metadata. + metadata (Dict[str, Any]): The collection metadata. """ - #: WLTS: The associated WLTS client to be used by the collection object. self._service = service - - super(Collections, self).__init__(metadata or {}) + super().__init__(metadata or {}) @property - def collection_type(self): + def collection_type(self) -> str: """Return the type of the collection.""" return self['collection_type'] @property - def description(self): + def description(self) -> str: """Return the description of the collection.""" return self['description'] @property - def detail(self): + def detail(self) -> str: """Return the detail of the collection.""" return self['detail'] @property - def name(self): + def name(self) -> str: """Return the name of the collection.""" return self['name'] @property - def title(self): + def title(self) -> str: """Return the title of the collection.""" return self['title'] @property - def period(self): + def period(self) -> Union[str, None]: """Return the period of the collection.""" - return self['period'] + return self.get('period') @property - def temporal_resolution(self): + def temporal_resolution(self) -> str: """Return the temporal resolution of the collection.""" return self['temporal_resolution'] @property - def spatial_extent(self): + def spatial_extent(self) -> Dict[str, Any]: """Return the spatial extent of the collection.""" return self['spatial_extent'] @property - def classification_system(self): + def classification_system(self) -> Dict[str, Any]: """Return the classification system of the collection.""" return self['classification_system'] - def __str__(self): + def __str__(self) -> str: """Return the string representation of the Collection object.""" return super().__str__() - def __repr__(self): + def __repr__(self) -> str: """Return the Collection object representation.""" wlts_repr = repr(self._service) + return f'Collection(service={wlts_repr}, metadata={super().__repr__()})' - text = f'Collection(service={wlts_repr}, metadata={super().__repr__()}' - - return text - - def _repr_html_(self): - """HTML repr.""" + def _repr_html_(self) -> str: + """HTML representation for IPython rich display.""" return Utils.render_html('collection.html', collection=self) diff --git a/wlts/templates/collection.html b/wlts/templates/collection.html index 119a8ea..eeb6691 100644 --- a/wlts/templates/collection.html +++ b/wlts/templates/collection.html @@ -19,7 +19,7 @@ Unit: {{collection.temporal_resolution['unit']}} Value: {{collection.temporal_resolution['value']}} -
+
Classification System:
Id: {{collection.classification_system['id']}} @@ -27,23 +27,27 @@ Name: {{collection.classification_system['name']}} Version: {{collection.classification_system['version']}}
-
+
Extent
- - - - - - - - - - - - - +
max_xmin_xmax_ymin_y
{{collection.spatial_extent['xmax']}}{{collection.spatial_extent['xmin']}}{{collection.spatial_extent['ymax']}}{{collection.spatial_extent['ymin']}}
+ + + + + + + + + + + + + + + +
max_xmin_xmax_ymin_y
{{collection.spatial_extent['xmax']}}{{collection.spatial_extent['xmin']}}{{collection.spatial_extent['ymax']}}{{collection.spatial_extent['ymin']}}
\ No newline at end of file diff --git a/wlts/templates/trajectory-item.html b/wlts/templates/trajectory-item.html index 46ad2a5..938afa0 100644 --- a/wlts/templates/trajectory-item.html +++ b/wlts/templates/trajectory-item.html @@ -19,12 +19,13 @@
  • Collections:
    • - {% for cl in tj.query['collections'] -%} + {% for cl in tj.query['collections'] %}
    • {{ cl }}
    • - {%- endfor %} + {% endfor %}
    {% endif %} + {% if 'geometry' in tj.trajectory %}
    Geometry: {{tj.trajectory['geometry']}} @@ -32,44 +33,48 @@ Trajectory
    + + + + + + + + + + {% for traj in tj.trajectory['trajectory'] %} - - - - - - - - {% for tj in tj.trajectory['trajectory']%} - - - - - + + + + {% endfor %} +
    ClassCollectionDateGeometry
    ClassCollectionDateGeometry
    {{tj['class']}}{{tj['collection']}}{{tj['date']}}{{tj['geometry']}}{{ traj['class'] }}{{ traj['collection'] }}{{ traj['date'] }}{{ traj['geometry'] }}
    -
    +
    {% else %} Trajectory
    + + + + + + + + + {% for traj in tj.trajectory %} - - - - - - - {% for tj in tj.trajectory%} - - - - + + + {% endfor %} +
    ClassCollectionDate
    ClassCollectionDate
    {{tj['class']}}{{tj['collection']}}{{tj['date']}}{{ traj['class'] }}{{ traj['collection'] }}{{ traj['date'] }}
    -
    +
    {% endif %} \ No newline at end of file diff --git a/wlts/templates/trajectory.html b/wlts/templates/trajectory.html index 2856d98..0555c2a 100644 --- a/wlts/templates/trajectory.html +++ b/wlts/templates/trajectory.html @@ -1,63 +1,53 @@ - -{% for tj in trajectories['trajectories'] %} - -
    - {% include 'trajectory-item.html' %} -
    -{% endfor %} - - \ No newline at end of file + .collapsible { + background-color: #eee; + color: #444; + cursor: pointer; + padding: 18px; + width: 100%; + border: none; + text-align: left; + outline: none; + font-size: 15px; + } + + .active, .collapsible:hover { + background-color: #ccc; + } + + .content { + padding: 0 18px; + display: none; + overflow: hidden; + background-color: #ffffff; + } + + .collapsible:after { + content: '+'; /* Unicode character for "plus" sign (+) */ + font-size: 18px; + color: black; + float: right; + margin-left: 5px; + } + + .active:after { + content: "-"; /* Unicode character for "minus" sign (-) */ + } + + + {% for tj in trajectories['trajectories'] %} + +
    + {% include 'trajectory-item.html' %} +
    + {% endfor %} + + \ No newline at end of file diff --git a/wlts/templates/wlts.html b/wlts/templates/wlts.html index d10ad0f..59bae7b 100644 --- a/wlts/templates/wlts.html +++ b/wlts/templates/wlts.html @@ -3,8 +3,8 @@
  • URL: {{ url }}
  • Collections:
    • - {% for cl in collections -%} + {% for cl in collections %}
    • {{ cl }}
    • - {%- endfor %} + {% endfor %}
    \ No newline at end of file diff --git a/wlts/trajectories.py b/wlts/trajectories.py index af655bd..21059c8 100644 --- a/wlts/trajectories.py +++ b/wlts/trajectories.py @@ -16,37 +16,33 @@ # along with this program. If not, see . # """A class that represents Trajectories in WLTS.""" +from typing import Any, Dict + +import pandas as pd + from .utils import Utils class Trajectories(dict): - """A class that represents a multiples trajectories in WLTS. + """A class that represents multiple trajectories in WLTS. - .. note:: - For more information about trajectory definition, please, refer to - `WLTS specification `_. + For more information about trajectory definition, refer to + `WLTS specification `_. """ - def __init__(self, data): + def __init__(self, data: Dict[str, Any]) -> None: """Create a Trajectories object. Args: - data: The trajectories. + data (Dict[str, Any]): The trajectories data. """ - super(Trajectories, self).__init__(data or {}) + super().__init__(data or {}) - def _repr_html_(self): - """Display the trajectories as HTML. - - This integrates a rich display in IPython. - """ + def _repr_html_(self) -> str: + """Display the trajectories as HTML for IPython rich display.""" return Utils.render_html('trajectory.html', trajectories=self) - def df(self, **options): + def df(self, **options: Any) -> pd.DataFrame: """Return the dataframe representation of the Trajectories object.""" - import pandas as pd - trjs_df = [trj.df() for trj in self['trajectories']] - - return pd.concat(trjs_df, axis=0).reset_index(drop=True) - + return pd.concat(trjs_df, axis=0).reset_index(drop=True) \ No newline at end of file diff --git a/wlts/trajectory.py b/wlts/trajectory.py index 0c6193e..23adf7b 100644 --- a/wlts/trajectory.py +++ b/wlts/trajectory.py @@ -16,6 +16,12 @@ # along with this program. If not, see . # """A class that represents Trajectory in WLTS.""" +from typing import Any, Dict, List + +import geopandas as gpd +import pandas as pd +from shapely.geometry import shape + from .utils import Utils @@ -27,7 +33,7 @@ class Trajectory(dict): `WLTS specification `_. """ - def __init__(self, data): + def __init__(self, data: Dict[str, Any]) -> None: """Create a Trajectory object. Args: @@ -36,31 +42,23 @@ def __init__(self, data): super(Trajectory, self).__init__(data or {}) @property - def trajectory(self, as_date=False, index: int = 1, fmt=''): + def trajectory(self, as_date=False, index: int = 1, fmt='') -> List[Dict[str, Any]]: """Return the trajectory associated with a location in space.""" return self['result']['trajectory'] @property - def query(self, as_date=False, fmt=''): + def query(self, as_date=False, fmt='') -> Dict[str, Any]: """Return the query.""" return self['query'] - def df(self, **options): + def df(self, **options) -> pd.DataFrame: """Return the dataframe representation of the Trajectory object.""" - import pandas as pd - return pd.DataFrame(self.trajectory) - def geodf(self, **options): + def geodf(self, **options) -> gpd.GeoDataFrame: """Return the geodataframe representation of the Trajectory object.""" if not all(['geom' in i for i in self.trajectory]): raise RuntimeError("Geometry field not exist! Verify if you pass geometry=True in service.trj!") - try: - import geopandas as gpd - from shapely.geometry import shape - except: - raise ImportError('You should install GeoPandas, Shapely and Descartes!') - gdf = gpd.GeoDataFrame(self.trajectory) gdf["geom"] = gdf.apply(lambda x: shape(x["geom"]), axis=1) @@ -70,7 +68,7 @@ def geodf(self, **options): return gdf - def _repr_html_(self): + def _repr_html_(self) -> str: """Display the trajectory as HTML. This integrates a rich display in IPython. diff --git a/wlts/utils.py b/wlts/utils.py index db3fac3..e908a88 100644 --- a/wlts/utils.py +++ b/wlts/utils.py @@ -16,6 +16,8 @@ # along with this program. If not, see . # """Utility functions for WLTS client library.""" +from typing import Any + import jinja2 from pkg_resources import resource_filename @@ -27,7 +29,16 @@ class Utils: """A class that represents a Utils in WLTS.""" @staticmethod - def render_html(template_name, **kwargs): - """Render Jinja2 HTML template.""" + def render_html(template_name: str, **kwargs: Any) -> str: + """ + Render a Jinja2 HTML template. + + Args: + template_name (str): The name of the template file to render. + **kwargs (Any): Arbitrary keyword arguments to be passed to the template. + + Returns: + str: The rendered HTML as a string. + """ template = templateEnv.get_template(template_name) - return template.render(**kwargs) + return template.render(**kwargs) \ No newline at end of file diff --git a/wlts/version.py b/wlts/version.py index 8d30db0..6ae5397 100644 --- a/wlts/version.py +++ b/wlts/version.py @@ -23,4 +23,6 @@ """ -__version__ = '1.1.0' +from importlib.metadata import version + +__version__ = version(__package__) diff --git a/wlts/wlts.py b/wlts/wlts.py index 2151002..b8cdb1d 100644 --- a/wlts/wlts.py +++ b/wlts/wlts.py @@ -21,7 +21,9 @@ trajectories for a given location. """ import json +from typing import Any, Dict, Iterator, Optional +import httpx import lccs import requests @@ -48,13 +50,18 @@ def __init__(self, url, lccs_url=None, access_token=None): access_token (str, optional): Authentication token to be used with the WLTS server. """ #: str: URL for the WLTS server. - self._url = url if url[-1] != '/' else url[0:-1] + self._url = url if url[-1] != "/" else url[0:-1] - #: str: Authentication token to be used with the WTSS server. - self._access_token = access_token + #: str: Authentication token to be used with the WLTS server. + self._access_token: str = access_token or "" + self._headers: Dict[str, str] = ( + {"x-api-key": self._access_token} if self._access_token else {} + ) #: str: URL for the LCCS server. - self._lccs_url = lccs_url if lccs_url else 'https://brazildatacube.dpi.inpe.br/lccs/' + self._lccs_url = ( + lccs_url if lccs_url else "https://brazildatacube.dpi.inpe.br/lccs/" + ) @property def collections(self): @@ -74,13 +81,17 @@ def _support_language(self): """Returns the languages supported by the service.""" import enum - response = requests.get(f'{self._url}/') + response = requests.get(f"{self._url}/") response.raise_for_status() data = response.json() - return enum.Enum('Language', {i['language']: i['language'] for i in data['supported_language']}, type=str) + return enum.Enum( + "Language", + {i["language"]: i["language"] for i in data["supported_language"]}, + type=str, + ) def tj(self, latitude, longitude, **options): """Retrieve the trajectory for a given location and time interval. @@ -111,41 +122,52 @@ def tj(self, latitude, longitude, **options): >>> ts.trajectory [{'class': 'Formação Florestal', 'collection': 'mapbiomas-v6', 'date': '2007'}, ...] """ + def validate_lat_long(lat, long): if (type(lat) not in (float, int)) or (type(long) not in (float, int)): raise ValueError("Arguments latitude and longitude must be numeric.") if (lat < -90.0) or (lat > 90.0): - raise ValueError('latitude is out-of range [-90,90]!') + raise ValueError("latitude is out-of range [-90,90]!") if (long < -180.0) or (long > 180.0): - raise ValueError('longitude is out-of range [-180,180]!') + raise ValueError("longitude is out-of range [-180,180]!") - invalid_parameters = set(options) - {"start_date", "end_date", "collections", "geometry", "target_system", - "language"} + invalid_parameters = set(options) - { + "start_date", + "end_date", + "collections", + "geometry", + "target_system", + "language", + } if invalid_parameters: - raise AttributeError('invalid parameter(s): {}'.format(invalid_parameters)) + raise AttributeError("invalid parameter(s): {}".format(invalid_parameters)) - if 'language' in options: + if "language" in options: self._support_l = self._support_language() - if options['language'] in [e.value for e in self._support_l]: + if options["language"] in [e.value for e in self._support_l]: pass else: - s = ', '.join([e for e in self.allowed_language]) - raise KeyError(f'Language not supported! Use: {s}') + s = ", ".join([e for e in self.allowed_language]) + raise KeyError(f"Language not supported! Use: {s}") if type(latitude) != list and type(longitude) != list: validate_lat_long(latitude, longitude) - data = self._trajectory(**{'latitude': latitude, 'longitude': longitude, **options}) + data = self._trajectory( + **{"latitude": latitude, "longitude": longitude, **options} + ) - for trj in data['result']['trajectory']: - trj['point_id'] = 1 + for trj in data["result"]["trajectory"]: + trj["point_id"] = 1 if "target_system" in options: - j = self._harmonize(data['result']['trajectory'], target_system=options["target_system"]) - data['result']['trajectory'] = json.loads(j) + j = self._harmonize( + data["result"]["trajectory"], target_system=options["target_system"] + ) + data["result"]["trajectory"] = json.loads(j) return Trajectory(data) @@ -157,10 +179,10 @@ def validate_lat_long(lat, long): for lat, long in zip(latitude, longitude): validate_lat_long(lat, long) - data = self._trajectory(**{'latitude': lat, 'longitude': long, **options}) + data = self._trajectory(**{"latitude": lat, "longitude": long, **options}) - for trj in data['result']['trajectory']: - trj['point_id'] = index + for trj in data["result"]["trajectory"]: + trj["point_id"] = index index = index + 1 result.append(Trajectory(data)) @@ -175,22 +197,25 @@ def _harmonize(self, data, target_system): df = pd.DataFrame(data) - for i in df['collection'].unique(): + for i in df["collection"].unique(): ds = self._describe_collection(i) mappings = lccs_service.mappings( system_source=f"{ds['classification_system']['id']}", - system_target=target_system) + system_target=target_system, + ) for map in mappings.mappings: - df.loc[(df['collection'] == i) & (df["class"] == map.source_class.title), [ - 'class']] = map.target_class.title + df.loc[ + (df["collection"] == i) & (df["class"] == map.source_class.title), + ["class"], + ] = map.target_class.title return df.to_json() def _list_collections(self): """Return the list of available collections.""" - result = self._get(self._url, op='list_collections') + result = self._get(self._url, op="list_collections") - return result['collections'] + return result["collections"] def _trajectory(self, **params): """Retrieve the trajectories of collections associated with a given location in space. @@ -211,7 +236,7 @@ def _trajectory(self, **params): Returns: Trajectory: A trajectory object as a dictionary. """ - return self._get(self._url, op='trajectory', **params) + return self._get(self._url, op="trajectory", **params) def _describe_collection(self, collection_id): """Describe a give collection. @@ -222,7 +247,9 @@ def _describe_collection(self, collection_id): :returns: Collection description. :rtype: dict """ - return self._get(self._url, op='describe_collection', collection_id=collection_id) + return self._get( + self._url, op="describe_collection", collection_id=collection_id + ) def __getitem__(self, key): """Get collection whose name is identified by the key. @@ -284,85 +311,103 @@ def plot(cls, dataframe, **parameters): try: import plotly.express as px except ImportError: - raise ImportError('You should install Plotly!') - - parameters.setdefault('marker_size', 10) - parameters.setdefault('title', 'Land Use and Cover Trajectory') - parameters.setdefault('title_y', 'Number of Points') - parameters.setdefault('legend_title_text', 'Class') - parameters.setdefault('date', 'Year') - parameters.setdefault('value', 'Collection') - parameters.setdefault('width', 950) - parameters.setdefault('height', 320) - parameters.setdefault('font_size', 12) - parameters.setdefault('type', 'scatter') + raise ImportError("You should install Plotly!") + + parameters.setdefault("marker_size", 10) + parameters.setdefault("title", "Land Use and Cover Trajectory") + parameters.setdefault("title_y", "Number of Points") + parameters.setdefault("legend_title_text", "Class") + parameters.setdefault("date", "Year") + parameters.setdefault("value", "Collection") + parameters.setdefault("width", 950) + parameters.setdefault("height", 320) + parameters.setdefault("font_size", 12) + parameters.setdefault("type", "scatter") # Parameters to update traces - parameters.setdefault('textfont_size', 12) - parameters.setdefault('textangle', 0) - parameters.setdefault('textposition', "auto") - parameters.setdefault('cliponaxis', False) + parameters.setdefault("textfont_size", 12) + parameters.setdefault("textangle", 0) + parameters.setdefault("textposition", "auto") + parameters.setdefault("cliponaxis", False) # Parameters to update layout - parameters.setdefault('text_auto', True) - parameters.setdefault('textposition', 'auto') - parameters.setdefault('opacity', 0.8) - parameters.setdefault('marker_line_width', 1.5) + parameters.setdefault("text_auto", True) + parameters.setdefault("textposition", "auto") + parameters.setdefault("opacity", 0.8) + parameters.setdefault("marker_line_width", 1.5) # Update column title bar plot - parameters.setdefault('bar_title', False) + parameters.setdefault("bar_title", False) df = dataframe.copy() - df['class'] = df['class'].astype('category') - df['date'] = df['date'].astype('category') - df['collection'] = df['collection'].astype('category') + df["class"] = df["class"].astype("category") + df["date"] = df["date"].astype("category") + df["collection"] = df["collection"].astype("category") def update_column_title(title): """Update the collection name with spaces and capitalize.""" new_title = (title.text.split("=")[-1]).capitalize() if len(new_title.split("_")) > 1: - return new_title.split("_")[0] + " " + new_title.split("_")[-1].capitalize() + return ( + new_title.split("_")[0] + + " " + + new_title.split("_")[-1].capitalize() + ) return new_title.split("_")[0] - if parameters['type'] == 'scatter': + if parameters["type"] == "scatter": # Validates the data for this plot type if len(dataframe.point_id.unique()) == 1: - fig = px.scatter(df, - y=['class', 'collection'], - x="date", color="class", - symbol="class", - labels={ - "date": parameters['date'], - "value": parameters['value'], - }, - title=parameters['title'], - width=parameters['width'], height=parameters['height']) - fig.update_traces(marker_size=parameters['marker_size']) - fig.update_layout(legend_title_text=parameters['legend_title_text'], font=dict( - size=parameters['font_size'], - )) + fig = px.scatter( + df, + y=["class", "collection"], + x="date", + color="class", + symbol="class", + labels={ + "date": parameters["date"], + "value": parameters["value"], + }, + title=parameters["title"], + width=parameters["width"], + height=parameters["height"], + ) + fig.update_traces(marker_size=parameters["marker_size"]) + fig.update_layout( + legend_title_text=parameters["legend_title_text"], + font=dict( + size=parameters["font_size"], + ), + ) return fig else: - raise ValueError("The scatter plot is for one point only! Please try another type: bar plot.") + raise ValueError( + "The scatter plot is for one point only! Please try another type: bar plot." + ) - if parameters['type'] == 'bar': + if parameters["type"] == "bar": # Validates the data for this plot type - Unique collection or multiples collections - if len(dataframe.collection.unique()) == 1 and len(dataframe.point_id.unique()) >= 1: - df_group = dataframe.groupby(['date', 'class']).count()['point_id'].unstack() + if ( + len(dataframe.collection.unique()) == 1 + and len(dataframe.point_id.unique()) >= 1 + ): + df_group = ( + dataframe.groupby(["date", "class"]).count()["point_id"].unstack() + ) fig = px.bar( df_group, - title=parameters['title'], - width=parameters['width'], - height=parameters['height'], - labels={"date": parameters['date'], "value": parameters['value']}, - text_auto=parameters['text_auto'], - ) + title=parameters["title"], + width=parameters["width"], + height=parameters["height"], + labels={"date": parameters["date"], "value": parameters["value"]}, + text_auto=parameters["text_auto"], + ) fig.update_layout( - legend_title_text=parameters['legend_title_text'], - font=dict(size=parameters['font_size']) + legend_title_text=parameters["legend_title_text"], + font=dict(size=parameters["font_size"]), ) fig.update_traces( textfont_size=parameters["textfont_size"], @@ -370,16 +415,20 @@ def update_column_title(title): textposition=parameters["textposition"], cliponaxis=parameters["cliponaxis"], opacity=parameters["opacity"], - marker_line_width=parameters["marker_line_width"] + marker_line_width=parameters["marker_line_width"], ) return fig - elif len(dataframe.collection.unique()) >= 1 and len(dataframe.point_id.unique()) >= 1: + elif ( + len(dataframe.collection.unique()) >= 1 + and len(dataframe.point_id.unique()) >= 1 + ): mydf = ( - dataframe.groupby(['date', 'collection']) - .apply(lambda x: x.groupby('class').count()) - .rename(columns={'collection': 'size', 'date': 'date_old'}).reset_index() + dataframe.groupby(["date", "collection"]) + .apply(lambda x: x.groupby("class").count()) + .rename(columns={"collection": "size", "date": "date_old"}) + .reset_index() ) fig = px.bar( @@ -390,11 +439,12 @@ def update_column_title(title): color="class", text="size", barmode="overlay", - width=parameters['width'], height=parameters['height'], + width=parameters["width"], + height=parameters["height"], labels={ - "size": parameters['title_y'], - "date": parameters['date'], - "collection": "Collection" + "size": parameters["title_y"], + "date": parameters["date"], + "collection": "Collection", }, ) @@ -404,17 +454,20 @@ def update_column_title(title): textposition=parameters["textposition"], cliponaxis=parameters["cliponaxis"], opacity=parameters["opacity"], - marker_line_width=parameters["marker_line_width"] - + marker_line_width=parameters["marker_line_width"], ) fig.update_layout( - legend_title_text='Class', - font=dict(size=12, ), - title_text=parameters['title'], + legend_title_text="Class", + font=dict( + size=12, + ), + title_text=parameters["title"], ) - if parameters['bar_title']: - fig.for_each_annotation(lambda a: a.update(text=update_column_title(a))) + if parameters["bar_title"]: + fig.for_each_annotation( + lambda a: a.update(text=update_column_title(a)) + ) return fig else: @@ -422,7 +475,7 @@ def update_column_title(title): def __str__(self): """Return the string representation of the WLTS object.""" - text = f'WLTS:\n\tURL: {self._url}' + text = f"WLTS:\n\tURL: {self._url}" return text @@ -448,7 +501,7 @@ def _repr_html_(self): """ cl_list = self._list_collections() - html = Utils.render_html('wlts.html', url=self._url, collections=cl_list) + html = Utils.render_html("wlts.html", url=self._url, collections=cl_list) return html @@ -468,20 +521,16 @@ def _get(self, url, op, **params): :rtype: dict :raises ValueError: If the response body does not contain a valid json. - """ - url_components = [url, op] - - params.setdefault('access_token', self._access_token) - - url = '/'.join(s.strip('/') for s in url_components) - - response = requests.get(url, params=params) - - response.raise_for_status() + """ + url = f"{self._url}/{op}" + params.setdefault("access_token", self._access_token) - content_type = response.headers.get('content-type') + with httpx.Client() as client: + response = client.get(url, params=params, headers=self._headers) + response.raise_for_status() - if content_type.count('application/json') == 0: - raise ValueError(f'HTTP response is not JSON: Content-Type: {content_type}') + content_type = response.headers.get("content-type", "") + if "application/json" not in content_type: + raise ValueError(f"HTTP Response is not JSON: Content-Type: {content_type}") - return response.json() + return response.json() \ No newline at end of file