From c69fe81ef8c25a1b40bf1630c8ec338ee1f0c30d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Wed, 21 Aug 2024 13:10:32 +0200 Subject: [PATCH] Initial commit --- .github/spell-ignore-words.txt | 0 .github/workflows/main.yaml | 43 ++ .gitignore | 4 + .pre-commit-config.yaml | 92 +++++ LICENSE | 22 ++ Makefile | 15 + README.md | 1 + ci/requirements.txt | 7 + geoservercloud/__init__.py | 3 + geoservercloud/geoservercloud.py | 657 +++++++++++++++++++++++++++++++ geoservercloud/gridsets/3857.xml | 92 +++++ geoservercloud/restservice.py | 81 ++++ geoservercloud/templates.py | 304 ++++++++++++++ geoservercloud/utils.py | 36 ++ poetry.lock | 560 ++++++++++++++++++++++++++ pyproject.toml | 42 ++ tests/test_smoke.py | 5 + 17 files changed, 1964 insertions(+) create mode 100644 .github/spell-ignore-words.txt create mode 100644 .github/workflows/main.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 ci/requirements.txt create mode 100644 geoservercloud/__init__.py create mode 100644 geoservercloud/geoservercloud.py create mode 100644 geoservercloud/gridsets/3857.xml create mode 100644 geoservercloud/restservice.py create mode 100644 geoservercloud/templates.py create mode 100644 geoservercloud/utils.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 tests/test_smoke.py diff --git a/.github/spell-ignore-words.txt b/.github/spell-ignore-words.txt new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..b522ac5 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,43 @@ +name: Continuous integration + +on: + push: + +env: + HAS_SECRETS: ${{ secrets.HAS_SECRETS }} + +jobs: + build: + name: Continuous integration + runs-on: ubuntu-22.04 + timeout-minutes: 15 + if: "!startsWith(github.event.head_commit.message, '[skip ci] ')" + + steps: + - uses: actions/checkout@v3 + - uses: camptocamp/initialise-gopass-summon-action@v2 + with: + ci-gpg-private-key: ${{secrets.CI_GPG_PRIVATE_KEY}} + github-gopass-ci-token: ${{secrets.GOPASS_CI_GITHUB_TOKEN}} + patterns: pypi docker + if: env.HAS_SECRETS == 'HAS_SECRETS' + + - run: echo "${HOME}/.local/bin" >> ${GITHUB_PATH} + - run: python3 -m pip install --user --requirement=ci/requirements.txt + + - uses: actions/cache@v3 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: "pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}\npre-commit-" + - run: pre-commit run --all-files + env: + SKIP: poetry-lock,isort + - run: git diff && false + if: failure() + + - name: Install + run: make install + + - name: Tests + run: make tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a6a246 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# ignore all python cache files recursively +**/__pycache__/ +/.pytest_cache/ +/.venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ce871d2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,92 @@ +repos: + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + additional_dependencies: + - prettier@2.8.4 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: detect-private-key + - id: check-merge-conflict + - id: check-ast + - id: debug-statements + - id: check-toml + - id: check-yaml + args: [--allow-multiple-documents] + - id: check-json + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + exclude: |- + (?x)( + (.*/)?poetry\.lock + |.github/changelog-generator-cache/.* + ) + args: + - --ignore-words=.github/spell-ignore-words.txt + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.10.0.1 + hooks: + - id: shellcheck + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.28.5 + hooks: + - id: check-github-actions + - id: check-github-workflows + - id: check-jsonschema + name: Check GitHub Workflows set timeout-minutes + files: ^\.github/workflows/[^/]+$ + types: + - yaml + args: + - --builtin-schema + - github-workflows-require-timeout + - id: check-renovate + additional_dependencies: + - pyjson5==1.6.6 # pypi + - repo: https://github.com/sirwart/ripsecrets + rev: v0.1.8 + hooks: + - id: ripsecrets + - repo: https://github.com/PyCQA/autoflake + rev: v2.3.1 + hooks: + - id: autoflake + - repo: https://github.com/asottile/pyupgrade + rev: v3.16.0 + hooks: + - id: pyupgrade + args: + - --py39-plus + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + args: + [ + --profile, + "black", + --filter-files, + --line-length, + "110", + --skip-gitignore, + --multi-line, + "3", + --split-on-trailing-comma, + ] + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black +ci: + autoupdate_schedule: quarterly + skip: + - copyright + - ripsecrets + - jsonschema-validator diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..254f248 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2024, Camptocamp SA +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a92089a --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: help +help: ## Display this help message + @echo "Usage: make " + @echo + @echo "Available targets:" + @grep --extended-regexp --no-filename '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s%s\n", $$1, $$2}' + +.PHONY: install +install: ## Install package + pip install . + +.PHONY: tests +tests: ## Run unit tests + pytest -vvv tests diff --git a/README.md b/README.md new file mode 100644 index 0000000..69ec5ba --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# python-geoservercloud diff --git a/ci/requirements.txt b/ci/requirements.txt new file mode 100644 index 0000000..84a2c7c --- /dev/null +++ b/ci/requirements.txt @@ -0,0 +1,7 @@ +c2cciutils[publish]==1.6.22 +poetry-dynamic-versioning[plugin]==1.4.0 +pre-commit==3.8.0 +pytest==8.3.2 +poetry-plugin-tweak-dependencies-version==1.5.2 +poetry-dynamic-versioning==1.4.0 +poetry-plugin-drop-python-upper-constraint==0.1.0 diff --git a/geoservercloud/__init__.py b/geoservercloud/__init__.py new file mode 100644 index 0000000..3287872 --- /dev/null +++ b/geoservercloud/__init__.py @@ -0,0 +1,3 @@ +from .geoservercloud import GeoServerCloud + +__all__: list[str] = ["GeoServerCloud"] diff --git a/geoservercloud/geoservercloud.py b/geoservercloud/geoservercloud.py new file mode 100644 index 0000000..f7764a6 --- /dev/null +++ b/geoservercloud/geoservercloud.py @@ -0,0 +1,657 @@ +import os.path +from pathlib import Path +from typing import Any + +import xmltodict +from owslib.map.wms130 import WebMapService_1_3_0 +from owslib.util import ResponseWrapper +from owslib.wmts import WebMapTileService +from requests import Response + +from geoservercloud import utils +from geoservercloud.restservice import RestService +from geoservercloud.templates import Templates + + +class GeoServerCloud: + def __init__( + self, + url: str = "http://localhost:9090/geoserver/cloud/", + user: str = "admin", + password: str = "geoserver", + ) -> None: + + self.url: str = url + self.user: str = user + self.password: str = password + self.auth: tuple[str, str] = (user, password) + self.rest_service: RestService = RestService(url, self.auth) + self.wms: WebMapService_1_3_0 | None = None + self.wmts: WebMapTileService | None = None + self.default_workspace: str | None = None + self.default_datastore: str | None = None + + @staticmethod + def workspace_wms_settings_path(workspace: str) -> str: + return f"/rest/services/wms/workspaces/{workspace}/settings.json" + + @staticmethod + def get_wmts_layer_bbox( + url: str, layer_name: str + ) -> tuple[float, float, float, float] | None: + wmts = WebMapTileService(url) + try: + return wmts[layer_name].boundingBoxWGS84 + except (KeyError, AttributeError): + return None + + def create_wms(self) -> None: + if self.default_workspace: + path: str = f"/{self.default_workspace}/wms" + else: + path = "/wms" + self.wms = WebMapService_1_3_0( + f"{self.url}{path}", + username=self.user, + password=self.password, + timeout=240, + ) + + def create_wmts(self) -> None: + path = "/gwc/service/wmts" + self.wmts = WebMapTileService( + f"{self.url}{path}", + version="1.0.0", + username=self.user, + password=self.password, + ) + + def create_workspace( + self, + workspace: str, + isolated: bool = False, + set_default_workspace: bool = False, + ) -> Response: + """ + Create a workspace in GeoServer, if it does not already exist. + It if exists, update it + """ + payload: dict[str, dict[str, Any]] = { + "workspace": { + "name": workspace, + "isolated": isolated, + } + } + response: Response = self.post_request("/rest/workspaces.json", json=payload) + if response.status_code == 409: + response = self.put_request( + f"/rest/workspaces/{workspace}.json", json=payload + ) + if set_default_workspace: + self.default_workspace = workspace + return response + + def delete_workspace(self, workspace: str) -> Response: + path: str = f"/rest/workspaces/{workspace}.json?recurse=true" + response: Response = self.delete_request(path) + if self.default_workspace == workspace: + self.default_workspace = None + self.wms = None + self.wmts = None + return response + + def recreate_workspace( + self, workspace: str, set_default_workspace: bool = False + ) -> None: + """ + Create a workspace in GeoServer, and first delete it if it already exists. + """ + self.delete_workspace(workspace) + self.create_workspace(workspace, set_default_workspace=set_default_workspace) + + def publish_workspace(self, workspace) -> Response: + path: str = f"{self.workspace_wms_settings_path(workspace)}" + + data: dict[str, dict[str, Any]] = Templates.workspace_wms(workspace) + return self.put_request(path, json=data) + + def set_default_locale_for_service( + self, workspace: str, locale: str | None + ) -> None: + path: str = self.workspace_wms_settings_path(workspace) + data: dict[str, dict[str, Any]] = { + "wms": { + "defaultLocale": locale, + } + } + self.put_request(path, json=data) + + def unset_default_locale_for_service(self, workspace) -> None: + self.set_default_locale_for_service(workspace, None) + + def create_pg_datastore( + self, + workspace: str, + datastore: str, + pg_host: str, + pg_port: int, + pg_db: str, + pg_user: str, + pg_password: str, + pg_schema: str = "public", + set_default_datastore: bool = False, + ) -> Response | None: + """ + Create a PostGIS datastore from the DB connection parameters, or update it if it already exist. + """ + response: None | Response = None + path = f"/rest/workspaces/{workspace}/datastores.json" + resource_path = f"/rest/workspaces/{workspace}/datastores/{datastore}.json" + payload: dict[str, dict[str, Any]] = Templates.postgis_data_store( + datastore=datastore, + pg_host=pg_host, + pg_port=pg_port, + pg_db=pg_db, + pg_user=pg_user, + pg_password=pg_password, + namespace=f"http://{workspace}", + pg_schema=pg_schema, + ) + if not self.resource_exists(resource_path): + response = self.post_request(path, json=payload) + else: + response = self.put_request(resource_path, json=payload) + + if set_default_datastore: + self.default_datastore = datastore + + return response + + def create_jndi_datastore( + self, + workspace: str, + datastore: str, + jndi_reference: str, + pg_schema: str = "public", + description: str | None = None, + set_default_datastore: bool = False, + ) -> Response | None: + """ + Create a PostGIS datastore from JNDI resource, or update it if it already exist. + """ + response: None | Response = None + path = f"/rest/workspaces/{workspace}/datastores.json" + resource_path = f"/rest/workspaces/{workspace}/datastores/{datastore}.json" + payload: dict[str, dict[str, Any]] = Templates.postgis_jndi_data_store( + datastore=datastore, + jndi_reference=jndi_reference, + namespace=f"http://{workspace}", + pg_schema=pg_schema, + description=description, + ) + if not self.resource_exists(resource_path): + response = self.post_request(path, json=payload) + else: + response = self.put_request(resource_path, json=payload) + + if set_default_datastore: + self.default_datastore = datastore + + return response + + def create_wmts_store( + self, + workspace: str, + name: str, + capabilities: str, + ) -> Response | None: + """ + Create a cascaded WMTS store, or update it if it already exist. + """ + response: None | Response = None + path = f"/rest/workspaces/{workspace}/wmtsstores.json" + resource_path = f"/rest/workspaces/{workspace}/wmtsstores/{name}.json" + payload = Templates.wmts_store(workspace, name, capabilities) + if not self.resource_exists(resource_path): + return self.post_request(path, json=payload) + else: + return self.put_request(resource_path, json=payload) + + def create_feature_type( + self, + layer: str, + workspace: str | None = None, + datastore: str | None = None, + title: str | dict = "Default title", + abstract: str | dict = "Default abstract", + attributes: dict = Templates.geom_point_attribute(), + epsg: int = 4326, + ) -> Response | None: + """ + Create a feature type or update it if it already exist. + """ + workspace = workspace or self.default_workspace + if not workspace: + raise ValueError("Workspace not provided") + datastore = datastore or self.default_datastore + if not datastore: + raise ValueError("Datastore not provided") + path: str = ( + f"/rest/workspaces/{workspace}/datastores/{datastore}/featuretypes.json" + ) + resource_path: str = ( + f"/rest/workspaces/{workspace}/datastores/{datastore}/featuretypes/{layer}.json" + ) + payload: dict[str, dict[str, Any]] = Templates.feature_type( + layer=layer, + workspace=workspace, + datastore=datastore, + attributes=utils.convert_attributes(attributes), + epsg=epsg, + ) + if type(title) is dict: + payload["featureType"]["internationalTitle"] = title + else: + payload["featureType"]["title"] = title + if type(abstract) is dict: + payload["featureType"]["internationalAbstract"] = abstract + else: + payload["featureType"]["abstract"] = abstract + + if not self.resource_exists(resource_path): + return self.post_request(path, json=payload) + else: + return self.put_request(resource_path, json=payload) + + def create_layer_group( + self, + group: str, + workspace: str | None, + layers: list[str], + title: str | dict, + abstract: str | dict, + epsg: int = 4326, + ) -> Response | None: + """ + Create a layer group if it does not already exist. + """ + workspace = workspace or self.default_workspace + if not workspace: + raise ValueError("Workspace not provided") + path: str = f"/rest/workspaces/{workspace}/layergroups.json" + resource_path: str = f"/rest/workspaces/{workspace}/layergroups/{group}.json" + payload: dict[str, dict[str, Any]] = Templates.layer_group( + group=group, + layers=layers, + workspace=workspace, + title=title, + abstract=abstract, + epsg=epsg, + ) + if not self.resource_exists(resource_path): + return self.post_request(path, json=payload) + else: + return self.put_request(resource_path, json=payload) + + def create_wmts_layer( + self, + workspace: str, + wmts_store: str, + native_layer: str, + published_layer: str | None = None, + epsg: int = 4326, + ) -> Response | None: + """ + Publish a remote WMTS layer if it does not already exist. + """ + if not published_layer: + published_layer = native_layer + if self.resource_exists( + f"/rest/workspaces/{workspace}/wmtsstores/{wmts_store}/layers/{published_layer}.json" + ): + return None + wmts_store_path = f"/rest/workspaces/{workspace}/wmtsstores/{wmts_store}.json" + capabilities_url = ( + self.get_request(wmts_store_path) + .json() + .get("wmtsStore") + .get("capabilitiesURL") + ) + wgs84_bbox = self.get_wmts_layer_bbox(capabilities_url, native_layer) + + path = f"/rest/workspaces/{workspace}/wmtsstores/{wmts_store}/layers" + payload = Templates.wmts_layer( + published_layer, native_layer, wgs84_bbox=wgs84_bbox, epsg=epsg + ) + + return self.post_request(path, json=payload) + + def publish_gwc_layer( + self, workspace: str, layer: str, epsg: int = 4326 + ) -> Response | None: + # Reload config to make sure GWC is aware of GeoServer layers + self.post_request( + "/gwc/rest/reload", + headers={"Content-Type": "application/json"}, + data="reload_configuration=1", + ) + payload = Templates.gwc_layer(workspace, layer, f"EPSG:{epsg}") + return self.put_request( + f"/gwc/rest/layers/{workspace}:{layer}.json", + json=payload, + ) + + def create_style_from_file( + self, + style: str, + file: str, + workspace: str | None = None, + ) -> Response: + """Create a style from a file, or update it if it already exists. + Supported file extensions are .sld and .zip.""" + path = ( + "/rest/styles" if not workspace else f"/rest/workspaces/{workspace}/styles" + ) + resource_path = ( + f"/rest/styles/{style}.json" + if not workspace + else f"/rest/workspaces/{workspace}/styles/{style}.json" + ) + + file_ext = os.path.splitext(file)[1] + if file_ext == ".sld": + content_type = "application/vnd.ogc.sld+xml" + elif file_ext == ".zip": + content_type = "application/zip" + else: + raise ValueError(f"Unsupported file extension: {file_ext}") + with open(f"{file}", "rb") as fs: + data: bytes = fs.read() + headers: dict[str, str] = {"Content-Type": content_type} + + if not self.resource_exists(resource_path): + return self.post_request(path, data=data, headers=headers) + else: + return self.put_request(resource_path, data=data, headers=headers) + + def set_default_layer_style( + self, layer: str, workspace: str, style: str + ) -> Response: + path = f"/rest/layers/{workspace}:{layer}.json" + data = {"layer": {"defaultStyle": {"name": style}}} + return self.put_request(path, json=data) + + def get_wms_capabilities( + self, workspace: str, accept_languages=None + ) -> dict[str, Any]: + path: str = f"/{workspace}/wms" + params: dict[str, str] = { + "service": "WMS", + "version": "1.3.0", + "request": "GetCapabilities", + } + if accept_languages: + params["AcceptLanguages"] = accept_languages + response: Response = self.get_request(path, params=params) + return xmltodict.parse(response.content) + + def get_wms_layers( + self, workspace: str, accept_languages: str | None = None + ) -> Any | dict[str, Any]: + capabilities: dict[str, Any] = self.get_wms_capabilities( + workspace, accept_languages + ) + try: + return capabilities["WMS_Capabilities"]["Capability"]["Layer"] + except KeyError: + return capabilities + + def get_wfs_capabilities(self, workspace: str) -> dict[str, Any]: + path: str = f"/{workspace}/wfs" + params: dict[str, str] = { + "service": "WFS", + "version": "1.1.0", + "request": "GetCapabilities", + } + response: Response = self.get_request(path, params=params) + return xmltodict.parse(response.content) + + def get_wfs_layers(self, workspace: str) -> Any | dict[str, Any]: + capabilities: dict[str, Any] = self.get_wfs_capabilities(workspace) + try: + return capabilities["wfs:WFS_Capabilities"]["FeatureTypeList"] + except KeyError: + return capabilities + + def get_map( + self, + layers: list[str], + bbox: tuple[float, float, float, float], + size: tuple[int, int], + srs: str = "EPSG:2056", + format: str = "image/png", + transparent: bool = True, + styles: list[str] | None = None, + language: str | None = None, + ) -> ResponseWrapper | None: + if not self.wms: + self.create_wms() + params: dict[str, Any] = { + "layers": layers, + "srs": srs, + "bbox": bbox, + "size": size, + "format": format, + "transparent": transparent, + "styles": styles, + } + if language is not None: + params["language"] = language + if self.wms: + return self.wms.getmap( + **params, + timeout=120, + ) + + def get_feature_info( + self, + layers: list[str], + bbox: tuple[float, float, float, float], + size: tuple[int, int], + srs: str = "EPSG:2056", + info_format: str = "application/json", + transparent: bool = True, + styles: list[str] | None = None, + xy: list[float] = [0, 0], + ) -> ResponseWrapper | None: + if not self.wms: + self.create_wms() + params = { + "layers": layers, + "srs": srs, + "bbox": bbox, + "size": size, + "info_format": info_format, + "transparent": transparent, + "styles": styles, + "xy": xy, + } + if self.wms: + return self.wms.getfeatureinfo( + **params, + ) + + def get_legend_graphic( + self, + layer: list[str], + format: str = "image/png", + language: str | None = None, + style: str | None = None, + workspace: str | None = None, + ) -> Response: + if not workspace: + path = "/wms" + else: + path: str = f"/{workspace}/wms" + params: dict[str, Any] = { + "service": "WMS", + "version": "1.3.0", + "request": "GetLegendGraphic", + "format": format, + "layer": layer, + } + if language: + params["language"] = language + if style: + params["style"] = style + response: Response = self.get_request(path, params=params) + return response + + def get_tile( + self, layer, format, tile_matrix_set, tile_matrix, row, column + ) -> ResponseWrapper | None: + if not self.wmts: + self.create_wmts() + if self.wmts: + return self.wmts.gettile( + layer=layer, + format=format, + tilematrixset=tile_matrix_set, + tilematrix=tile_matrix, + row=row, + column=column, + ) + + def create_role(self, role_name: str): + self.post_request(f"/rest/security/roles/role/{role_name}") + + def delete_role(self, role_name: str): + self.delete_request(f"/rest/security/roles/role/{role_name}") + + def create_role_if_not_exists(self, role_name: str) -> Response | None: + if self.role_exists(role_name): + return None + return self.create_role(role_name) + + def role_exists(self, role_name: str) -> bool: + response = self.get_request( + f"/rest/security/roles", headers={"Accept": "application/json"} + ) + roles = response.json().get("roles", []) + return role_name in roles + + def create_acl_admin_rule( + self, + priority: int = 0, + access: str = "ADMIN", + role: str | None = None, + user: str | None = None, + workspace: str | None = None, + ) -> Response: + path = "/acl/api/adminrules" + return self.post_request( + path, + json={ + "priority": priority, + "access": access, + "role": role, + "user": user, + "workspace": workspace, + }, + ) + + def delete_acl_admin_rule(self, id: int) -> Response: + path = f"/acl/api/adminrules/id/{id}" + return self.delete_request(path) + + def delete_all_acl_admin_rules(self) -> Response: + path = "/acl/api/adminrules" + return self.delete_request(path) + + def create_acl_rule( + self, + priority: int = 0, + access: str = "DENY", + role: str | None = None, + service: str | None = None, + workspace: str | None = None, + ) -> Response: + path = "/acl/api/rules" + return self.post_request( + path, + json={ + "priority": priority, + "access": access, + "role": role, + "service": service, + "workspace": workspace, + }, + ) + + def delete_all_acl_rules(self) -> Response: + path = "/acl/api/rules" + return self.delete_request(path) + + def create_or_update_resource(self, path, resource_path, payload) -> Response: + if not self.resource_exists(resource_path): + return self.post_request(path, json=payload) + else: + return self.put_request(resource_path, json=payload) + + def create_gridset(self, epsg: int) -> Response | None: + resource_path: str = f"/gwc/rest/gridsets/EPSG:{epsg}.xml" + if self.resource_exists(resource_path): + return None + file_path: Path = Path(__file__).parent / "gridsets" / f"{epsg}.xml" + headers: dict[str, str] = {"Content-Type": "application/xml"} + try: + data: bytes = file_path.read_bytes() + except FileNotFoundError: + raise ValueError(f"No gridset definition found for EPSG:{epsg}") + return self.put_request(resource_path, data=data, headers=headers) + + def resource_exists(self, path: str) -> bool: + # GeoServer raises a 500 when posting to a datastore or feature type that already exists, so first do + # a get request + response = self.get_request(path) + return response.status_code == 200 + + def get_request( + self, + path, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + ) -> Response: + return self.rest_service.get(path, params=params, headers=headers) + + def post_request( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + json: dict[str, Any] | None = None, + data: bytes | None = None, + ) -> Response: + return self.rest_service.post( + path, params=params, headers=headers, json=json, data=data + ) + + def put_request( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + json: dict[str, dict[str, Any]] | None = None, + data: bytes | None = None, + ) -> Response: + return self.rest_service.put( + path, params=params, headers=headers, json=json, data=data + ) + + def delete_request( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + ) -> Response: + return self.rest_service.delete(path, params=params, headers=headers) diff --git a/geoservercloud/gridsets/3857.xml b/geoservercloud/gridsets/3857.xml new file mode 100644 index 0000000..2a43207 --- /dev/null +++ b/geoservercloud/gridsets/3857.xml @@ -0,0 +1,92 @@ + + + EPSG:3857 + This well-known scale set has been defined to be compatible with Google Maps and + Microsoft Live Map projections and zoom levels. Level 0 allows representing the whole world + in a single 256x256 pixels. The next level represents the whole world in 2x2 tiles of + 256x256 pixels and so on in powers of 2. Scale denominator is only accurate near the + equator. + + 3857 + + + + -2.003750834E7 + -2.003750834E7 + 2.003750834E7 + 2.003750834E7 + + + false + + 156543.03390625 + 78271.516953125 + 39135.7584765625 + 19567.87923828125 + 9783.939619140625 + 4891.9698095703125 + 2445.9849047851562 + 1222.9924523925781 + 611.4962261962891 + 305.74811309814453 + 152.87405654907226 + 76.43702827453613 + 38.218514137268066 + 19.109257068634033 + 9.554628534317017 + 4.777314267158508 + 2.388657133579254 + 1.194328566789627 + 0.5971642833948135 + 0.29858214169740677 + 0.14929107084870338 + 0.07464553542435169 + 0.037322767712175846 + 0.018661383856087923 + 0.009330691928043961 + 0.004665345964021981 + 0.0023326729820109904 + 0.0011663364910054952 + 5.831682455027476E-4 + 2.915841227513738E-4 + 1.457920613756869E-4 + + 1.0 + 2.8E-4 + + EPSG:3857:0 + EPSG:3857:1 + EPSG:3857:2 + EPSG:3857:3 + EPSG:3857:4 + EPSG:3857:5 + EPSG:3857:6 + EPSG:3857:7 + EPSG:3857:8 + EPSG:3857:9 + EPSG:3857:10 + EPSG:3857:11 + EPSG:3857:12 + EPSG:3857:13 + EPSG:3857:14 + EPSG:3857:15 + EPSG:3857:16 + EPSG:3857:17 + EPSG:3857:18 + EPSG:3857:19 + EPSG:3857:20 + EPSG:3857:21 + EPSG:3857:22 + EPSG:3857:23 + EPSG:3857:24 + EPSG:3857:25 + EPSG:3857:26 + EPSG:3857:27 + EPSG:3857:28 + EPSG:3857:29 + EPSG:3857:30 + + 256 + 256 + false + diff --git a/geoservercloud/restservice.py b/geoservercloud/restservice.py new file mode 100644 index 0000000..3e6cd6e --- /dev/null +++ b/geoservercloud/restservice.py @@ -0,0 +1,81 @@ +from typing import Any + +import requests + + +class RestService: + def __init__(self, url: str, auth: tuple[str, str]) -> None: + self.url: str = url + self.auth: tuple[str, str] = auth + + def get( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + ) -> requests.Response: + response: requests.Response = requests.get( + f"{self.url}{path}", + params=params, + headers=headers, + auth=self.auth, + ) + if response.status_code != 404: + response.raise_for_status() + return response + + def post( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + json: dict[str, dict[str, Any]] | None = None, + data: bytes | None = None, + ) -> requests.Response: + + response: requests.Response = requests.post( + f"{self.url}{path}", + params=params, + headers=headers, + json=json, + data=data, + auth=self.auth, + ) + if response.status_code != 409: + response.raise_for_status() + return response + + def put( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + json: dict[str, dict[str, Any]] | None = None, + data: bytes | None = None, + ) -> requests.Response: + response: requests.Response = requests.put( + f"{self.url}{path}", + params=params, + headers=headers, + json=json, + data=data, + auth=self.auth, + ) + response.raise_for_status() + return response + + def delete( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + ) -> requests.Response: + response: requests.Response = requests.delete( + f"{self.url}{path}", + params=params, + headers=headers, + auth=self.auth, + ) + if response.status_code != 404: + response.raise_for_status() + return response diff --git a/geoservercloud/templates.py b/geoservercloud/templates.py new file mode 100644 index 0000000..5db4589 --- /dev/null +++ b/geoservercloud/templates.py @@ -0,0 +1,304 @@ +from typing import Any + +EPSG_BBOX = { + 2056: { + "nativeBoundingBox": { + "crs": {"$": f"EPSG:2056", "@class": "projected"}, + "maxx": 2837016.9329778464, + "maxy": 1299782.763494124, + "minx": 2485014.052451379, + "miny": 1074188.6943776933, + }, + "latLonBoundingBox": { + "crs": "EPSG:4326", + "maxx": 10.603307860867739, + "maxy": 47.8485348773655, + "minx": 5.902662003204146, + "miny": 45.7779277267225, + }, + }, + 4326: { + "nativeBoundingBox": { + "crs": {"$": f"EPSG:4326", "@class": "projected"}, + "maxx": 180, + "maxy": 90, + "minx": -180, + "miny": -90, + }, + "latLonBoundingBox": { + "crs": "EPSG:4326", + "maxx": 180, + "maxy": 90, + "minx": -180, + "miny": -90, + }, + }, +} + + +class Templates: + + @staticmethod + def workspace_wms(workspace: str) -> dict[str, dict[str, Any]]: + return { + "wms": { + "workspace": {"name": workspace}, + "enabled": True, + "name": "WMS", + "versions": { + "org.geotools.util.Version": [ + {"version": "1.1.1"}, + {"version": "1.3.0"}, + ] + }, + "citeCompliant": False, + "schemaBaseURL": "http://schemas.opengis.net", + "verbose": False, + "bboxForEachCRS": False, + "watermark": { + "enabled": False, + "position": "BOT_RIGHT", + "transparency": 100, + }, + "interpolation": "Nearest", + "getFeatureInfoMimeTypeCheckingEnabled": False, + "getMapMimeTypeCheckingEnabled": False, + "dynamicStylingDisabled": False, + "featuresReprojectionDisabled": False, + "maxBuffer": 0, + "maxRequestMemory": 0, + "maxRenderingTime": 0, + "maxRenderingErrors": 0, + "maxRequestedDimensionValues": 100, + "cacheConfiguration": { + "enabled": False, + "maxEntries": 1000, + "maxEntrySize": 51200, + }, + "remoteStyleMaxRequestTime": 60000, + "remoteStyleTimeout": 30000, + "defaultGroupStyleEnabled": True, + "transformFeatureInfoDisabled": False, + "autoEscapeTemplateValues": False, + } + } + + @staticmethod + def postgis_data_store( + datastore: str, + pg_host: str, + pg_port: int, + pg_db: str, + pg_user: str, + pg_password: str, + namespace: str, + pg_schema: str = "public", + ) -> dict[str, dict[str, Any]]: + return { + "dataStore": { + "name": datastore, + "connectionParameters": { + "entry": [ + {"@key": "dbtype", "$": "postgis"}, + {"@key": "host", "$": pg_host}, + {"@key": "port", "$": pg_port}, + {"@key": "database", "$": pg_db}, + {"@key": "user", "$": pg_user}, + {"@key": "passwd", "$": pg_password}, + {"@key": "schema", "$": pg_schema}, + { + "@key": "namespace", + "$": namespace, + }, + {"@key": "Expose primary keys", "$": "true"}, + ] + }, + } + } + + @staticmethod + def postgis_jndi_data_store( + datastore: str, + jndi_reference: str, + namespace: str, + pg_schema: str = "public", + description: str | None = None, + ) -> dict[str, dict[str, Any]]: + return { + "dataStore": { + "name": datastore, + "description": description, + "connectionParameters": { + "entry": [ + {"@key": "dbtype", "$": "postgis"}, + { + "@key": "jndiReferenceName", + "$": jndi_reference, + }, + { + "@key": "schema", + "$": pg_schema, + }, + { + "@key": "namespace", + "$": namespace, + }, + {"@key": "Expose primary keys", "$": "true"}, + ] + }, + } + } + + @staticmethod + def wmts_store( + workspace: str, name: str, capabilities: str + ) -> dict[str, dict[str, Any]]: + return { + "wmtsStore": { + "name": name, + "type": "WMTS", + "capabilitiesURL": capabilities, + "workspace": {"name": workspace}, + "enabled": True, + "metadata": {"entry": {"@key": "useConnectionPooling", "text": True}}, + } + } + + @staticmethod + def geom_point_attribute() -> dict[str, Any]: + return { + "geom": { + "type": "Point", + "required": True, + } + } + + @staticmethod + def feature_type( + layer: str, + workspace: str, + datastore: str, + attributes: list[dict], + epsg: int = 4326, + ) -> dict[str, dict[str, Any]]: + return { + "featureType": { + "name": layer, + "nativeName": layer, + "srs": f"EPSG:{epsg}", + "enabled": True, + "store": { + "name": f"{workspace}:{datastore}", + }, + "attributes": { + "attribute": attributes, + }, + "nativeBoundingBox": { + "crs": EPSG_BBOX[epsg]["nativeBoundingBox"]["crs"], + "maxx": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxx"], + "maxy": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxy"], + "minx": EPSG_BBOX[epsg]["nativeBoundingBox"]["minx"], + "miny": EPSG_BBOX[epsg]["nativeBoundingBox"]["miny"], + }, + "latLonBoundingBox": { + "crs": EPSG_BBOX[epsg]["latLonBoundingBox"]["crs"], + "maxx": EPSG_BBOX[epsg]["latLonBoundingBox"]["maxx"], + "maxy": EPSG_BBOX[epsg]["latLonBoundingBox"]["maxy"], + "minx": EPSG_BBOX[epsg]["latLonBoundingBox"]["minx"], + "miny": EPSG_BBOX[epsg]["latLonBoundingBox"]["miny"], + }, + } + } + + @staticmethod + def layer_group( + group: str, + layers: list[str], + workspace: str, + title: str | dict[str, Any], + abstract: str | dict[str, Any], + epsg: int = 4326, + ) -> dict[str, dict[str, Any]]: + template = { + "layerGroup": { + "name": group, + "workspace": {"name": workspace}, + "mode": "SINGLE", + "publishables": { + "published": [ + {"@type": "layer", "name": f"{workspace}:{layer}"} + for layer in layers + ] + }, + "styles": {"style": [{"name": ""}] * len(layers)}, + "bounds": { + "minx": EPSG_BBOX[epsg]["nativeBoundingBox"]["minx"], + "maxx": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxx"], + "miny": EPSG_BBOX[epsg]["nativeBoundingBox"]["miny"], + "maxy": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxy"], + "crs": f"EPSG:{epsg}", + }, + "enabled": True, + "advertised": True, + } + } + if title: + if type(title) is dict: + template["layerGroup"]["internationalTitle"] = title + else: + template["layerGroup"]["title"] = title + if abstract: + if type(abstract) is dict: + template["layerGroup"]["internationalAbstract"] = abstract + else: + template["layerGroup"]["abstract"] = abstract + return template + + @staticmethod + def wmts_layer( + name: str, + native_name: str, + epsg: int = 4326, + wgs84_bbox: tuple[float, float, float, float] | None = None, + ) -> dict[str, dict[str, Any]]: + template = { + "wmtsLayer": { + "advertised": True, + "enabled": True, + "name": name, + "nativeName": native_name, + "projectionPolicy": "FORCE_DECLARED", + "serviceConfiguration": False, + "simpleConversionEnabled": False, + "srs": f"EPSG:{epsg}", + } + } + if wgs84_bbox: + template["wmtsLayer"]["latLonBoundingBox"] = { + "crs": "EPSG:4326", + "minx": wgs84_bbox[0], + "maxx": wgs84_bbox[2], + "miny": wgs84_bbox[1], + "maxy": wgs84_bbox[3], + } + if epsg == 4326: + template["wmtsLayer"]["nativeBoundingBox"] = { + "crs": "EPSG:4326", + "minx": wgs84_bbox[0], + "maxx": wgs84_bbox[2], + "miny": wgs84_bbox[1], + "maxy": wgs84_bbox[3], + } + return template + + @staticmethod + def gwc_layer( + workspace: str, layer: str, gridset: str + ) -> dict[str, dict[str, Any]]: + return { + "GeoServerLayer": { + "name": f"{workspace}:{layer}", + "enabled": "true", + "gridSubsets": {"gridSubset": [{"gridSetName": gridset}]}, + } + } diff --git a/geoservercloud/utils.py b/geoservercloud/utils.py new file mode 100644 index 0000000..cecbf78 --- /dev/null +++ b/geoservercloud/utils.py @@ -0,0 +1,36 @@ +def java_binding(data_type: str) -> str: + match data_type: + case "string": + return "java.lang.String" + case "integer": + return "java.lang.Integer" + case "float": + return "java.lang.Float" + case "datetime": + return "java.util.Date" + case "Point": + return "org.locationtech.jts.geom.Point" + case "Line": + return "org.locationtech.jts.geom.LineString" + case "Polygon": + return "org.locationtech.jts.geom.Polygon" + case "MultiPolygon": + return "org.locationtech.jts.geom.MultiPolygon" + case _: + return "java.lang.String" + + +def convert_attributes(attributes: dict) -> list[dict]: + geoserver_attributes = [] + for name, data in attributes.items(): + required: bool = data.get("required", False) + geoserver_attributes.append( + { + "name": name, + "minOccurs": int(required), + "maxOccurs": 1, + "nillable": not required, + "binding": java_binding(data["type"]), + } + ) + return geoserver_attributes diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..a682f8c --- /dev/null +++ b/poetry.lock @@ -0,0 +1,560 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "lxml" +version = "5.3.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, + {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, + {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, + {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, + {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, + {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, + {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, + {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, + {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, + {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, + {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, + {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, + {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, + {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, + {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, + {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, + {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, + {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, + {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, + {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, + {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, + {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml-html-clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.11)"] + +[[package]] +name = "owslib" +version = "0.31.0" +description = "OGC Web Service utility library" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "OWSLib-0.31.0-py2.py3-none-any.whl", hash = "sha256:ab54d20974bd8ddd24ee5539c46a7829ec9e77ed0976724f635d1a3a321d3a39"}, + {file = "OWSLib-0.31.0.tar.gz", hash = "sha256:2ed6540087445cc57d905138a590b6ae58624ec7661b5c1682ed4e3303bcd150"}, +] + +[package.dependencies] +lxml = "*" +python-dateutil = ">=1.5" +pytz = "*" +pyyaml = "*" +requests = ">=1.0" + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.2.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +category = "main" +optional = false +python-versions = ">=3.4" +files = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<4.0" +content-hash = "23e2455d1da8f608869338e21ce0c5e7d9120fe7df6252027a0ce6a98f3e8043" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2d99415 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[tool.poetry] +name = "geoservercloud" +version = "0.0.0" +description = "Lightweight Python client to interact with GeoServer Cloud REST API, GeoServer ACL and OGC services" +authors = ["Camptocamp "] +license = "BSD-2-Clause" +readme = "README.md" +packages = [{include = "geoservercloud"}] + +[tool.poetry.dependencies] +python = ">=3.9,<4.0" +requests = "2.32.3" +OWSLib = "0.31.0" +xmltodict = "0.13.0" + +[tool.poetry.group.dev.dependencies] +pytest = "8.2.2" + +[build-system] +requires = [ + "poetry-core>=1.0.0", + "poetry-dynamic-versioning[plugin]", + "poetry-plugin-tweak-dependencies-version" +] +build-backend = "poetry.core.masonry.api" + +[tool.poetry-dynamic-versioning] +enable = true +vcs = "git" +pattern = "^(?P\\d+(\\.\\d+)*)" +format-jinja = """ +{%- if env.get("VERSION_TYPE") == "version_branch" -%} +{{serialize_pep440(bump_version(base, 1 if env.get("IS_MASTER") == "TRUE" else 2), dev=distance)}} +{%- elif distance == 0 -%} +{{serialize_pep440(base)}} +{%- else -%} +{{serialize_pep440(bump_version(base), dev=distance)}} +{%- endif -%} +""" + +[tool.poetry-plugin-tweak-dependencies-version] +default = "present" diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..f714349 --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,5 @@ +from geoservercloud import GeoServerCloud + + +def test_smoke(): + GeoServerCloud()