diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..101f694 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ +[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/fabianazioti/lccs.py@b.9.1", + #"lccs @ git+https://github.com/brazil-data-cube/lccs.py@v0.9.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/setup.py b/setup.py index b9605d8..28ef42d 100644 --- a/setup.py +++ b/setup.py @@ -18,99 +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', - 'rich>=13.9.2', - 'lccs @ git+https://github.com/fabianazioti/lccs.py@b.9.1', - #'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/wlts/cli.py b/wlts/cli.py index 168b603..61232a0 100644 --- a/wlts/cli.py +++ b/wlts/cli.py @@ -24,6 +24,9 @@ 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 @@ -43,13 +46,22 @@ def __init__(self): console = Console() - @click.group() -@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.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): @@ -59,79 +71,115 @@ 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: - console.print(f'[bold black]Server: [green]{config.url}[/green]', style="bold") - console.print('[black]\tRetrieving the list of available coverages...[/black]') - - table = Table(title="Available Collections", show_header=True, header_style="bold magenta") + 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: - table.add_row(collection) + describe_collection = config.service[collection] + table.add_row(collection, describe_collection["title"]) console.print(table) - console.print('[black]\tFinished![/black]') + console.print("[black]\tFinished![/black]") else: for collection in config.service.collections: - console.print(f'[green]{collection}[/green]', style="bold") + 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 collection metadata.""" - import json - if verbose: - console.print(f'[bold black]Server: [green]{config.url}[/green]', style="bold") - console.print('[black]\tRetrieving the collection metadata...[/black]') - # Retrieve the collection metadata cv = config.service[collection] - # Convert the metadata to a formatted JSON string - formatted_json = json.dumps(cv, indent=4,ensure_ascii=False) + if verbose: + console.print(f"[bold black]Server: [green]{config.url}[/green]", style="bold") + console.print("[black]\tRetrieving the collection metadata...[/black]") + + tree = Tree(cv["title"], guide_style="bold cyan") - # 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 - if verbose: - console.print('[black]\tFinished![/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: console.print(f"[bold black]Server: [green]{config.url}[/green]") - console.print('[black]\tRetrieving trajectory...[/black]') + console.print("[black]\tRetrieving trajectory...[/black]") # Prepare query parameters args = dict() @@ -179,10 +227,12 @@ def trajectory(config: Config, verbose, collections, start_date, end_date, latit # Add rows from the trajectory data for entry in retval.trajectory: - table.add_row(entry['class'], entry['collection'], entry['date'], str(entry['point_id'])) + table.add_row( + entry["class"], entry["collection"], entry["date"], str(entry["point_id"]) + ) # Display the table console.print(table) if verbose: - console.print(f'[black]\tFinished in {total_time:.2f} seconds![/black]') \ No newline at end of file + console.print(f"[black]\tFinished in {total_time:.2f} seconds![/black]") 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