[
  {
    "path": ".github/workflows/python-publish.yml",
    "content": "# This workflow will upload a Python Package using Twine when a release is created\n# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries\n\n# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\nname: Publish geoserver-rest to PyPI / GitHub\n\non:\n  push:\n    tags:\n      - \"v*\"\n\npermissions:\n  contents: read\n\njobs:\n  deploy:\n    runs-on: ubuntu-22.04\n\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Python\n        uses: actions/setup-python@v3\n        with:\n          python-version: \"3.x\"\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install build\n      - name: Build package\n        run: python -m build\n      - name: Publish package\n        uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29\n        with:\n          user: __token__\n          password: ${{ secrets.PYPI_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/python-test.yml",
    "content": "name: Run tests\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    branches: [master]\n  # schedule:\n  #   - cron: \"0 0 * * *\"\n\npermissions:\n  contents: read\n\njobs:\n\n  test-ubuntu:\n\n    runs-on: ubuntu-22.04\n\n    strategy:\n      matrix:\n        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']\n\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Set up Python\n      uses: actions/setup-python@v5.1.0\n      with:\n        cache: 'pip'\n        python-version: ${{ matrix.python-version }}\n\n    - name: Install dependencies\n      run: |\n        sudo apt-add-repository ppa:ubuntugis/ppa\n        sudo apt-get update\n        sudo apt-get install gdal-bin libgdal-dev\n        python -m pip install --upgrade pip\n        python -m pip install GDAL==`gdal-config --version` \n        python -m pip install -r requirements_dev.txt\n\n    - name: Set up docker-compose\n      run: |\n        docker compose -f tests/docker-compose.yaml up -d\n        sleep 60  # Geoserver takes quite a long time to boot up and there is no healthcheck\n\n    - name: Test with pytest\n      uses: dariocurr/pytest-summary@main\n      with:\n        paths: tests/test_geoserver.py\n      env:\n        DB_HOST: postgis\n\n    - name: Upload test summary\n      uses: actions/upload-artifact@v4\n      with:\n        name: test-summary-linux\n        path: test-summary-linux.md\n      if: always()\n\n  #test-windows:\n  #\n  #  runs-on: windows-latest\n  #\n  #  steps:\n  #    - uses: actions/checkout@v4\n  #\n  #    - name: Set up Python 3.10\n  #      uses: actions/setup-python@v3\n  #      with:\n  #        python-version: '3.10'\n  #\n  #    - name: Set up PostGIS\n  #      run: |\n  #\n  #        # Install PostGIS (PostgreSQL comes on the GitHub Actions runner by default but lacks PostGIS control files and dependencies)\n  #        netsh advfirewall firewall show rule name=\"Allow Localhost 5432\"\n  #        Invoke-WebRequest -Uri \"http://download.osgeo.org/postgis/windows/pg14/postgis-bundle-pg14x64-setup-3.4.1-1.exe\" -OutFile \"postgis-installer.exe\"\n  #        Start-Process \"postgis-installer.exe\" -ArgumentList \"/S /D=C:\\Program Files\\PostgreSQL\\14\" -Wait -NoNewWindow\n  #        & \"C:\\Program Files\\PostgreSQL\\14\\bin\\pg_ctl.exe\" -D \"C:\\Program Files\\PostgreSQL\\14\\data\" start\n  #        & \"C:\\Program Files\\PostgreSQL\\14\\bin\\psql.exe\" -U postgres -c \"CREATE DATABASE geodb;\"\n  #        & \"C:\\Program Files\\PostgreSQL\\14\\bin\\psql.exe\" -U postgres -c \"CREATE USER geodb_user WITH ENCRYPTED PASSWORD 'geodb_pass';\"\n  #        & \"C:\\Program Files\\PostgreSQL\\14\\bin\\psql.exe\" -U postgres -c \"GRANT ALL PRIVILEGES ON DATABASE geodb TO geodb_user;\"\n  #        & \"C:\\Program Files\\PostgreSQL\\14\\bin\\psql.exe\" -U postgres -d geodb -c \"CREATE EXTENSION postgis;\"\n  #\n  #    - name: Set up Tomcat/GeoServer\n  #      run: |\n  #\n  #        # Configure firewall to allow connectiosn to localhost port 8080 (might not be necessary)\n  #        netsh advfirewall firewall add rule name=\"Allow Localhost 5432\" dir=in action=allow protocol=TCP localport=5432\n  #\n  #        # Download and install Apache Tomcat (need version 9 because the JRE on the GitHub Actions runner is incompatible with 10+)\n  #        curl -L https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.88/bin/apache-tomcat-9.0.88-windows-x64.zip -o tomcat.zip\n  #        Expand-Archive -Path tomcat.zip -DestinationPath \"C:\\\"\n  #        $directory = Get-ChildItem -Path \"C:\\\" -Directory | Where-Object { $_.Name -like \"*apache-tomcat*\" } | Select-Object -First 1\n  #        if ($directory) { Rename-Item -Path $directory.FullName -NewName \"Tomcat\" }\n  #\n  #        # Download and install Geoserver, then move it to the Tomcat directory\n  #        # Version-locked to 2.22.0 beacause of Java 8\n  #        curl -L https://sourceforge.net/projects/geoserver/files/GeoServer/2.22.0/geoserver-2.22.0-war.zip/download -o geoserver.zip\n  #        Expand-Archive -Path geoserver.zip -DestinationPath \"C:\\GeoServer\"\n  #        cp C:\\GeoServer\\geoserver.war C:\\Tomcat\\webapps\\geoserver.war\n  #\n  #        # Set env vars for Tomcat and run it (it takes a little while, so wait 30 seconds after)\n  #        $env:CATALINA_BASE = \"C:\\Tomcat\"\n  #        $env:CATALINA_HOME = \"C:\\Tomcat\"\n  #        $env:CATALINA_TMPDIR = \"C:\\Tomcat\\temp\"\n  #        C:\\Tomcat\\bin\\startup.bat\n  #        Start-Sleep -Seconds 30\n  #\n  #      shell: pwsh\n  #\n  #    - name: Install Miniconda\n  #      run: |\n  #        curl -O https://repo.anaconda.com/miniconda/Miniconda3-py39_4.10.3-Windows-x86_64.exe\n  #        Start-Process -FilePath \"Miniconda3-py39_4.10.3-Windows-x86_64.exe\" -ArgumentList '/InstallationType=JustMe /RegisterPython=0 /S /D=\"%UserProfile%\\Miniconda3\"' -Wait -NoNewWindow\n  #        & \"$env:UserProfile\\Miniconda3\\Scripts\\conda\" init powershell\n  #      shell: pwsh\n  #\n  #    - name: Configure Conda environment\n  #      run: |\n  #        $env:PATH = \"$env:UserProfile\\Miniconda3;$env:UserProfile\\Miniconda3\\Scripts;$env:UserProfile\\Miniconda3\\Library\\bin;$env:PATH\"\n  #        conda update conda -y\n  #        conda create -n geospatial python=3.10 -y\n  #        conda activate geospatial\n  #        conda install -c conda-forge gdal>=3.4.1 -y\n  #        python -m pip install --upgrade pip\n  #        pip install -r requirements_dev.txt\n  #      shell: pwsh\n  #\n  #    #- name: Test with pytest\n  #    #  uses: dariocurr/pytest-summary@main\n  #    #  with:\n  #    #    paths: tests/test_geoserver.py\n  #    #  env:\n  #    #    DB_HOST: postgis\n  #\n  #    - name: Test with pytest\n  #      run: |\n  #        conda activate geospatial\n  #        pytest tests/test_geoserver.py\n  #\n  #    - name: Upload test summary\n  #      uses: actions/upload-artifact@v3\n  #      with:\n  #        name: test-summary-windows\n  #        path: test-summary-windows.md\n  #      if: always()\n"
  },
  {
    "path": ".gitignore",
    "content": "style.sld\nFunctionsToImplement.py\nrecord.txt\npackage_test.py\ntest.py\n.idea/\n\n# Created by https://www.toptal.com/developers/gitignore/api/python\n# Edit at https://www.toptal.com/developers/gitignore?templates=python\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\nrecord.txt\nFunctionsToImplement.py\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\npytestdebug.log\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\ndoc/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\ntest.ipynb\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n.vscode/\n.direnv/\nvenv/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# End of https://www.toptal.com/developers/gitignore/api/python\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "default_language_version:\n    python: python3\n\nrepos:\n-   repo: https://github.com/asottile/pyupgrade\n    rev: v3.3.1\n    hooks:\n    -   id: pyupgrade\n        language_version: python3\n\n-   repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.4.0\n    hooks:\n    -   id: check-yaml\n        language_version: python3\n        exclude: conda-recipe/meta.yaml\n    -   id: debug-statements\n        language_version: python3\n    -   id: end-of-file-fixer\n        language_version: python3\n        exclude: .ipynb|.txt|.sld\n    -   id: trailing-whitespace\n        language_version: python3\n        exclude: .txt|.sld\n\n-   repo: https://github.com/ambv/black\n    rev: 22.12.0\n    hooks:\n    -   id: black\n        args: [\"--target-version\", \"py36\"]\n\n-   repo: https://github.com/timothycrosley/isort\n    rev: 5.12.0\n    hooks:\n      - id: isort\n        args: [\"--profile\", \"black\", \"--filter-files\"]\n\n\n-   repo: https://github.com/pycqa/flake8\n    rev: 6.0.0\n    hooks:\n    -   id: flake8\n        args: ['--config=setup.cfg']\n\n-   repo: https://github.com/pycqa/pydocstyle\n    rev: 6.2.1\n    hooks:\n    -   id: pydocstyle\n        language_version: python3\n        args: ['--convention=numpy', '--match=\"(?!test_).*\\.py\"']\n\n-   repo: meta\n    hooks:\n    -   id: check-hooks-apply\n    -   id: check-useless-excludes\n\nci:\n    autoupdate_schedule: quarterly\n    skip: []\n    submodules: false\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.12\"\n    # You can also specify other tool versions:\n    # nodejs: \"20\"\n    # rust: \"1.70\"\n    # golang: \"1.20\"\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: docs/source/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  # fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\n# formats:\n#   - pdf\n#   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n  install:\n    - requirements: docs/requirements-docs.txt\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at iamtekson@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guidelines\n\nGeoserver-rest is an open source library written in python and contributors are needed to keep this library moving forward. Any kind of contributions are welcome.\n\n# Guidelines\n\n1. Please use the request library for the http request.\n2. One feature per pull request.\n3. Please add the update about your PR on the [change log documentation](https://github.com/gicait/geoserver-rest/blob/master/docs/source/change_log.rst#master-branch) as well.\n"
  },
  {
    "path": "DEV_DOCS.md",
    "content": "# Documentation\n\n### Publishing the python package\n\nFor publishing the python package, follow following steps,\n\n1. Install twine `pip install twine`\n2. Generate packages and create dist folder by running setup.py file `python setup.py bdist_wheel sdist`\n3. Push to PyPI `twine upload dist/*`\n\n### Pre-commit Reference\n\nTo install the additional libraries for dev (testing, formatting, etc.) run `pip install --editable .[dev]`\n\n[`pre-commit`](https://pre-commit.com/) is a pretty standard tooling-helper for automating the formatting of code (`isort`, `black`, `end-of-file-fixer`, `trailing-space-fixer`, etc.) and evaluating any potential issues (`flake8`). This essentially makes all code contributions look the same no matter what in order to favour readability/maintainability.\n\nInstalling and running `pre-commit` is fairly automatic. After installing the requirements-dev.txt, run:\n\n```bash\npre-commit install\n```\n\nand the environments will be managed automatically. Any calls to git commit will run the checks. If something changes, you need to simply run git commit a second time and it should be good.\n\nSome checks will require changes (e.g. imports of `pdb` are a violation, unused imports, unused initialized objects, etc.). If these are needed by design you can do either of the following:\n\nTo commit the files and ignore the checks (not great as checks will fail for future commits and for others):\n\n```bash\ngit commit --no-verify -m \"my message\"\n```\n\nIf you want the violation to be ignored, place a \\_\\_# noqa (precisely: two empty spaces, #, one empty space, noqa) next to the affected line.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020, Geoinformatics Center, Asian Institute of Technology\nCopyright (c) 2020, Tek Kshetri\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Python wrapper for GeoServer REST API\n\n[![Downloads](https://pepy.tech/badge/geoserver-rest)](https://pepy.tech/project/geoserver-rest)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![flake8](https://img.shields.io/badge/linter-flake8-green)](https://flake8.pycqa.org/)\n[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)\n[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)\n\n## Full documentation\n\nThe documentation for this project is moved here: [https://geoserver-rest.readthedocs.io/](https://geoserver-rest.readthedocs.io/).\n\n## Overview\n\nThe `geoserver-rest` package is useful for the management of geospatial data in [GeoServer](http://geoserver.org/). The package is useful for the creating, updating and deleting geoserver workspaces, stores, layers, and style files.\n\n## Installation\n\n```python\nconda install -c conda-forge geoserver-rest\n```\n\nFor the `pip` installation, check the [official documentation of geoserver-rest](https://geoserver-rest.readthedocs.io/en/latest/installation.html)\n\n## Some examples\n\nPlease check the [https://geoserver-rest.readthedocs.io/](https://geoserver-rest.readthedocs.io/) for full documentation.\n\n```python\n# Import the library\nfrom geo.Geoserver import Geoserver\n\n# Initialize the library\ngeo = Geoserver('http://127.0.0.1:8080/geoserver', username='admin', password='geoserver')\n\n# For creating workspace\ngeo.create_workspace(workspace='demo')\n\n# For uploading raster data to the geoserver\ngeo.create_coveragestore(layer_name='layer1', path=r'path\\to\\raster\\file.tif', workspace='demo')\n\n# For creating postGIS connection and publish postGIS table\ngeo.create_featurestore(store_name='geo_data', workspace='demo', db='postgres', host='localhost', pg_user='postgres',\n                        pg_password='admin')\ngeo.publish_featurestore(workspace='demo', store_name='geo_data', pg_table='geodata_table_name')\n\n# For uploading SLD file and connect it with layer\ngeo.upload_style(path=r'path\\to\\sld\\file.sld', workspace='demo')\ngeo.publish_style(layer_name='geoserver_layer_name', style_name='sld_file_name', workspace='demo')\n\n# delete workspace\ngeo.delete_workspace(workspace='demo')\n\n# delete layer\ngeo.delete_layer(layer_name='layer1', workspace='demo')\n\n# delete style file\ngeo.delete_style(style_name='style1', workspace='demo')\n```\n\nTo create the dynamic style you need to install the extra dependencies as, `gdal`, `seaborn` and `matplotlib`. The below functions will be only available after installing the full package `geoserver-rest[all]` or `geoserver-rest[style]`,\n\n```python\n# For creating the style file for raster data dynamically and connect it with layer\ngeo.create_coveragestyle(raster_path=r'path\\to\\raster\\file.tiff', style_name='style_1', workspace='demo',\n                         color_ramp='RdYiGn')\ngeo.publish_style(layer_name='geoserver_layer_name', style_name='raster_file_name', workspace='demo')\n\n# For creating outline featurestyle\ngeo.create_outline_featurestyle(style_name='style1', color='#ff0000')\n\n```\n\n## Contribution\n\nGeoserver-rest is the open source library written in python and contributors are needed to keep this library moving forward. Any kind of contributions are welcome. Here are the basic rule for the new contributors:\n\n1. Please use the request library for the http request.\n2. One feature per pull request (If the PR is huge, you need to create a issue and discuss).\n3. Please add the update about your PR on the [change log documentation](https://github.com/gicait/geoserver-rest/blob/master/docs/source/change_log.rst#master-branch) as well.\n\n## Citation\n\nFull paper is available here: https://doi.org/10.5194/isprs-archives-XLVI-4-W2-2021-91-2021\n\n```\n@Article{isprs-archives-XLVI-4-W2-2021-91-2021,\n      AUTHOR = {Tek Bahadur Kshetri, Angsana Chaksana and Shraddha Sharma},\n      TITLE = {THE ROLE OF OPEN-SOURCE PYTHON PACKAGE GEOSERVER-REST IN WEB-GIS DEVELOPMENT},\n      JOURNAL = {The International Archives of the Photogrammetry, Remote Sensing and Spatial Information Sciences},\n      VOLUME = {XLVI-4/W2-2021},\n      YEAR = {2021},\n      PAGES = {91--96},\n      URL = {https://www.int-arch-photogramm-remote-sens-spatial-inf-sci.net/XLVI-4-W2-2021/91/2021/},\n      DOI = {10.5194/isprs-archives-XLVI-4-W2-2021-91-2021}\n  }\n```\n"
  },
  {
    "path": "conda-recipe/bld.bat",
    "content": "cd %RECIPE_DIR%\\..\n%PYTHON% setup.py install --single-version-externally-managed --record=record.txt\n"
  },
  {
    "path": "conda-recipe/build.sh",
    "content": "$PYTHON setup.py install\n"
  },
  {
    "path": "conda-recipe/meta.yaml",
    "content": "{% set data = load_setup_py_data() %}\npackage:\n  name: \"geoserver-rest\"\n  version: {{ data.get('version') }}\n\nbuild:\n  # entry_points:\n  #   - anaconda = binstar_client.scripts.cli:main\n  #   - binstar = binstar_client.scripts.cli:main\n  #   - conda-server = binstar_client.scripts.cli:main\n\nsource:\n  path: ./../\n\nrequirements:\n  build:\n    - python\n    - setuptools\n\n  run:\n    - python\n    - setuptools\n    - gdal\n    - seaborn\n    - requests\n    - pygments\n    - matplotlib\n    - xmltodict\n\nabout:\n  home: https://github.com/gicait/geoserver-rest\n  license: MIT\n  license_familY: MIT\n  license_file: LICENSE\n  summary: \"The package for management of geospatial data in GeoServer\"\n\nextra:\n  recipe-maintainers:\n    - iamtekson\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = source\nBUILDDIR      = build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset SOURCEDIR=source\nset BUILDDIR=build\n\nif \"%1\" == \"\" goto help\n\n%SPHINXBUILD% >NUL 2>NUL\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.http://sphinx-doc.org/\n\texit /b 1\n)\n\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\ngoto end\n\n:help\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\n\n:end\npopd\n"
  },
  {
    "path": "docs/requirements-docs.txt",
    "content": "jinja2\nsphinx\nsphinx_rtd_theme\nreadthedocs-sphinx-ext\nm2r\nmistune\nsphinxcontrib-httpdomain\nsphinxcontrib-openapi\ngeopandas\nseaborn\nrequests\nxmltodict\n"
  },
  {
    "path": "docs/source/about.rst",
    "content": "About geoserver-rest!\n=====================\n\nWhat is geoserver-rest?\n^^^^^^^^^^^^^^^^^^^^^^^\n\nThe geoserver-rest package is useful for the management of geospatial data in `GeoServer <http://geoserver.org/>`_.\nThis package is useful for creating, updating and deleting geoserver workspaces, stores, layers, and style files.\n\nFor a live example of geoserver-rest in action, check out the video tutorial on geoserver-rest below:\n\n.. raw:: html\n\n    <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/nXvzmbGukeE\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n\n\nCurrent version\n---------------\n\nCurrent version: v1.5.1\n"
  },
  {
    "path": "docs/source/acknowledgements.rst",
    "content": "Acknowledgements\n================\n\nCreated and managed by `Tek Bahadur Kshetri <https://github.com/iamtekson>`_ for the activities of the Geoinformatics Center of Asian Institute of Technology, Thailand.\n"
  },
  {
    "path": "docs/source/advanced_uses.rst",
    "content": "Advanced uses for automation\n============================\n\nThe following code will first convert all the ``.rst`` data format inside ``C:\\Users\\gic\\Desktop\\etlIa\\`` folder, into ``tiff`` format and then upload all the ``tiff`` files to the GeoServer.\n\n\n.. code-block:: python3\n\n    from geo.Geoserver import Geoserver\n    from osgeo import gdal\n    import glob\n    import os\n\n    geo = Geoserver('http://localhost:8080/geoserver', username='admin', password='geoserver')\n\n    rst_files = glob.glob(r'C:\\Users\\gic\\Desktop\\etlIa\\*.rst')\n    geo.create_workspace('geonode')\n\n    for rst in rst_files:\n        file_name = os.path.basename(rst)\n        src = gdal.Open(rst)\n        tiff = r'C:\\Users\\tek\\Desktop\\try\\{}'.format(file_name)\n        gdal.Translate(tiff, src)\n        geo.create_coveragestore(layer_name=file_name, path=tiff, workspace='geonode')    #, overwrite=True\n\n\nThe following code will upload all the ``tiff`` files (with extension .tiff or .tif) located in ``data/landuse`` to the GeoServer.\n\n\n.. code-block:: python3\n\n    from geo.Geoserver import Geoserver\n    import glob\n    import os\n\n    geo = Geoserver('http://localhost:8080/geoserver', username='admin', password='geoserver')\n    geo.create_workspace('test')\n    tiff_files = glob.glob('data/landuse/*.tiff') + glob.glob('data/landuse/*.tif')\n\n    for tiff in tiff_files:\n        file_name = os.path.basename(tiff)\n\n        # Removing extension for layer name\n        temp = os.path.splitext(file_name)\n        layer_name = temp[0]\n\n        # Will overwrite layer if it exists\n        geo.create_coveragestore(layer_name=layer_name, path=tiff, workspace='test')\n\n        print(file_name + ' uploaded to geoserver')\n"
  },
  {
    "path": "docs/source/change_log.rst",
    "content": "Change Log\n=============\n\n``Master branch``\n^^^^^^^^^^^^^^^^^\n* New method `create_gpkg_datastore`\n* Bugfixes for `add_layer_to_layergroup` and `remove_layer_from_layergroup`\n* New method `remove_layer_from_layergroup`\n\n``[v2.4.1 - 2023-01-14]``\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n* New method `add_layer_to_layergroup` (see issue `#102 <https://github.com/gicait/geoserver-rest/issues/102>`)\n* Allow deletion of layergroups from workspaces (see issue `#100 <https://github.com/gicait/geoserver-rest/issues/100>`) and add unittests for the layergroup methods.\n* Fix json-bug in create_coveragestore method (see issue `#86 <https://github.com/gicait/geoserver-rest/issues/86>`)\n\n``[v2.4.0 - 2023-01-10]``\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n* Fix the import issue, close `#76 <https://github.com/gicait/geoserver-rest/issues/76>`_\n* Removed the ``rest`` from Geoserver class URL, Revert back to previous state. Close `#77 <https://github.com/gicait/geoserver-rest/issues/76>`_\n* Add Optional Parameter for ``title`` to ``publish_featurestore`` function\n* Update on git hooks `#94 <https://github.com/gicait/geoserver-rest/pull/94>`_, `#92 <https://github.com/gicait/geoserver-rest/pull/92>`_\n* Exception handeling in a better way `#93 <https://github.com/gicait/geoserver-rest/pull/93>`_\n\n``[v2.3.0 - 2022-05-06]``\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n* Params for `delete_workspace` and `delete_style` changed to `{recursive: true}`\n* Added methods to use REST API for user/group service CRUD operations.\n* Removed ``key_column`` parameter and added ``srid`` parameter (coordinate system of the layer, default is 4326) from ``publish_featurestore_sqlview`` function\n* Solved the Bug `#73 <https://github.com/gicait/geoserver-rest/issues/73>`_ and `#69 <https://github.com/gicait/geoserver-rest/issues/69>`_\n* ``create_layergroup`` function added\n* ``update_layergroup`` function added\n* ``delete_layergroup`` function added\n*  Added layer and workspace checks to layergroup methods\n\n\n``[V2.1.2 - 2021-10-14]``\n^^^^^^^^^^^^^^^^^^^^^^^^^\n* ``create_featurestore`` bug on Expose primary key fixed close #56.\n* ``create_featurestore`` will now support all the options from geoserver.\n\n\n``[V2.0.0 - 2021-08-14]``\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n* Expose primary key option for datastore in ``create_featurestore`` function\n* Time dimention support for the coverage store\n* Bug fixing for the ``.tiff`` datastore\n* Added the request.content to error messages in order to get more information about error\n\n\n``[V2.0.0 - 2021-05-28]``\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n* Fully replaced the `pycurl <http://pycurl.io/>`_ dependency with `request` and `psycopg2 <https://www.psycopg.org/>`_\n* Dropped the PostgreSQL functionalities (deleted ``geo/Postgres.py`` file). I think the functionalities of PostgreSQL is outside the scope of this library. So I initiated the seperated library `postgres-helper <https://postgres-helper.readthedocs.io/en/latest/>`_\n* Documentation adjustments\n* The ``overwrite`` options removed from ``create_coveragestore``, ``create_coveragestyle`` and other style functions\n\n\n``[V1.6.0 - 2021-04-13]``\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n* Documentation adjustments (bunch of Sphinx-docs formatting fixes and English corrections)\n* Add black and pre-commit\n* ``create_coveragestore`` function parameter name ``lyr_name`` changed to ``layer_name``\n* Pytest examples, docstrings and typed calls added\n\n\n``[V1.5.2] - 2020-11-03``\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n* **1. create_datastore** This function can create the datastore from `.shp`, `.gpkg`, WFS url and directory containing `.shp`.\n* **2. create_shp_datastore** This function will be useful for uploading the shapefile and publishing the shapefile as a layer. This function will upload the data to the geoserver ``data_dir`` in ``h2`` database structure and publish it as a layer.\n* Update on docs\n"
  },
  {
    "path": "docs/source/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\n\nsys.path.insert(0, os.path.abspath(\"../../\"))\n\n# -- Project information -----------------------------------------------------\n\nproject = \"geoserver-rest\"\ncopyright = \"2021, Tek Bahadur Kshetri\"\nauthor = \"Tek Bahadur Kshetri\"\n\n# The full version, including alpha/beta/rc tags\nrelease = \"2.5.1\"\n\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\"sphinx_rtd_theme\", \"sphinx.ext.autodoc\"]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = [\"_templates\"]\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = []\n\nautosummary_generate = True\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = \"sphinx_rtd_theme\"\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = [\"_static\"]\n"
  },
  {
    "path": "docs/source/contribution.rst",
    "content": "Contribution\n=============\n\nGeoserver-rest is an open source library written in python and contributors are needed to keep this library moving forward. Any kind of contributions are welcome.\n"
  },
  {
    "path": "docs/source/geo.rst",
    "content": "Python API Reference\n====================\n\n**Disclamer**: The documentation in the code was initially written by people, but was then passed through an AI large\nlanguage model (specifically ChatGPT-4o) to fill in gaps and correct minor mistakes. The results were also validated by\na person, but it is possible that an AI \"hallucination\" has occurred that was not caught and resulted in an incorrect\ndocumentation. Please `report an issue <https://github.com/gicait/geoserver-rest/issues>`_ you find one.\n\nThe ``Geoserver`` class\n------------------------\n\n.. automodule:: geo.Geoserver\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\n\nThe ``Style`` functions\n------------------------\n\n.. automodule:: geo.Style\n   :members:\n   :undoc-members:\n   :show-inheritance:\n"
  },
  {
    "path": "docs/source/how_to_use.rst",
    "content": "How to use\n===========\n\nThis library is built for getting, creating, updating and deleting workspaces, coveragestores, featurestores, and styles. Some examples are shown below.\n\nGetting started with `geoserver-rest`\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThis following step is used to initialize the library. It takes parameters as geoserver url, username, password.\n\n.. code-block:: python\n\n    from geo.Geoserver import Geoserver\n    geo = Geoserver('http://127.0.0.1:8080/geoserver', username='admin', password='geoserver')\n\nCreating workspaces\n-------------------\n\n.. code-block:: python\n\n    geo.create_workspace(workspace='demo')\n\nCreating coveragestores\n-----------------------\n\nIt is helpful for publishing the raster data to the geoserver. Here if you don't pass the ``lyr_name`` parameter, it will take the raster file name as the layer name.\n\n.. code-block:: python\n\n    geo.create_coveragestore(layer_name='layer1', path=r'path\\to\\raster\\file.tif', workspace='demo')\n\n\n.. note::\n    If your raster is not loading correctly, please make sure you assign the coordinate system for your raster file.\n\n    If the ``layer_name`` already exists in geoserver, it will automatically overwrite the previous one.\n\n\nCreating and publishing featurestores and featurestore layers\n-------------------------------------------------------------\n\n.. _create_featurestore:\n\nIt is used for connecting the ``PostGIS`` with geoserver and publish this as a layer. It is only useful for vector data. The postgres connection parameters should be passed as the parameters. For publishing the PostGIS tables, the ``pg_table`` parameter represent the table name in postgres\n\n.. code-block:: python3\n\n    geo.create_featurestore(store_name='geo_data', workspace='demo', db='postgres', host='localhost', pg_user='postgres', pg_password='admin')\n    geo.publish_featurestore(workspace='demo', store_name='geo_data', pg_table='geodata_table_name')\n\n\nThe new function ``publish_featurestore_sqlview`` is available from geoserver-rest version ``1.3.0``. The function can be run by using following command,\n\n.. code-block:: python3\n\n    sql = 'SELECT name, id, geom FROM post_gis_table_name'\n    geo.publish_featurestore_sqlview(store_name='geo_data', name='view_name', sql=sql, key_column='name', workspace='demo')\n\n\nCreating and publishing shapefile datastore layers\n--------------------------------------------------\n\nThe ``create_shp datastore`` function will be useful for uploading the shapefile and publishing the shapefile as a layer. This function will upload the data to the geoserver ``data_dir`` in ``h2`` database structure and publish it as a layer. The layer name will be same as the shapefile name.\n\n.. code-block:: python3\n\n    geo.create_shp_datastore(path=r'path/to/zipped/shp/file.zip', store_name='store', workspace='demo')\n\nCreating and publishing datastore layers\n----------------------------------------\n\nThe ``create_datastore`` function will create the datastore for the specific data. After creating the datastore, you need to publish it as a layer by using ``publish_featurestore`` function. It can take the following type of data path:\n\n1. Path to shapefile (`.shp`) file;\n2. Path to GeoPackage (`.gpkg`) file;\n3. WFS url (e.g. http://localhost:8080/geoserver/wfs?request=GetCapabilities) or;\n4. Directory containing shapefiles.\n\nIf you have PostGIS datastore, please use :ref:`create_featurestore <create_featurestore>` function.\n\n.. code-block:: python3\n\n    geo.create_datastore(name=\"ds\", path=r'path/to/shp/file_name.shp', workspace='demo')\n    geo.publish_featurestore(workspace='demo', store_name='ds', pg_table='file_name')\n\nIf your data is coming from ``WFS`` url, then use this,\n\n.. code-block:: python3\n\n    geo.create_datastore(name=\"ds\", path='http://localhost:8080/geoserver/wfs?request=GetCapabilities', workspace='demo')\n    geo.publish_featurestore(workspace='demo', store_name='ds', pg_table='wfs_layer_name')\n\n\nCreating Layer Groups\n-------------------------------\nA layer group is a grouping of layers and styles that can be accessed as a single layer in a WMS GetMap request.\nLayer groups can be created either inside a workspace, or globally without a workspace.\n\nYou can create a layer group from layers that have been uploaded previously with the ``create_layergroup`` method.\n\n.. code-block:: python3\n\n  # create a new layergroup from 2 existing layers\n    geo.create_layergroup(\n      name = \"my_fancy_layergroup\",\n      mode = \"single\",\n      title = \"My Fancy Layergroup Title\",\n      abstract_text = \"This is a very fancy Layergroup\",\n      layers = [\"fancy_layer_1\", \"fancy_layer_2\"],\n      workspace = \"my_space\", #None if you want to create a layergroup outside the workspace\n      keywords = [\"list\", \"of\", \"keywords\"]\n      )\n\n  # add another layer\n    geo.add_layer_to_layergroup(\n      layergroup_name = \"my_fancy_layergroup\",\n      layergroup_workspace = \"my_space\",\n      layer_name = \"superfancy_layer\",\n      layer_workspace = \"my_space\"\n    )\n\n  # remove a layer\n    geo.remove_layer_from_layergroup(\n      layergroup_name = \"my_fancy_layergroup\",\n      layergroup_workspace = \"my_space\",\n      layer_name = \"superfancy_layer\",\n      layer_workspace = \"my_space\"\n    )\n\n\nUploading and publishing styles\n-------------------------------\n\n**WARNING:** As of version 2.9.0, the required dependency ``gdal``, ``matplotlib`` and ``seaborn`` was converted into an optional dependency. Fresh installations of this library will require that you then install ``gdal``, ``matplotlib`` and ``seaborn`` yourself with ``pip install gdal matplotlib seaborn``.\n\n\nIt is used for uploading ``SLD`` files and publish style. If the style name already exists, you can pass the parameter ``overwrite=True`` to overwrite it. The name of the style will be name of the uploaded file name.\n\nBefore uploading ``SLD`` file, please check the version of your sld file. By default the version of sld will be ``1.0.0``. As I noticed, by default the QGIS will provide the .sld file of version ``1.0.0`` for raster data version ``1.1.0`` for vector data.\n\n\n.. code-block:: python3\n\n    geo.upload_style(path=r'path\\to\\sld\\file.sld', workspace='demo')\n    geo.publish_style(layer_name='geoserver_layer_name', style_name='sld_file_name', workspace='demo')\n\nCreating and applying dynamic styles based on the raster coverages\n------------------------------------------------------------------\n\n**WARNING:** As of version 2.9.0, the required dependency ``gdal`` was converted into an optional dependency. Fresh installations of this library will require that you then install ``gdal`` yourself with ``pip install gdal``.\n\nIt is used to create the style file for raster data. You can get the ``color_ramp`` name from `matplotlib colormaps <https://matplotlib.org/3.3.0/tutorials/colors/colormaps.html>`_. By default ``color_ramp='RdYlGn'`` (red to green color ramp).\n\n.. code-block:: python\n\n    geo.create_coveragestyle(raster_path=r'path\\to\\raster\\file.tiff', style_name='style_1', workspace='demo', color_ramp='RdBu_r')\n    geo.publish_style(layer_name='geoserver_layer_name', style_name='raster_file_name', workspace='demo')\n\n\n.. note::\n    If you have your own custom color and the custom label, you can pass the ``values:color`` pair as below to generate the map with dynamic legend.\n\n\n.. code-block:: python\n\n    c_ramp = {\n        'label 1 value': '#ffff55',\n        'label 2 value': '#505050',\n        'label 3 value': '#404040',\n        'label 4 value': '#333333'\n    }\n    geo.create_coveragestyle(raster_path=r'path\\to\\raster\\file.tiff',\n                                style_name='style_2',\n                                workspace='demo',\n                                color_ramp=c_ramp,\n                                cmap_type='values')\n\n    # you can also pass this list of color if you have your custom colors for the ``color_ramp``\n    '''\n    geo.create_coveragestyle(raster_path=r'path\\to\\raster\\file.tiff',\n                                style_name='style_3',\n                                workspace='demo',\n                                color_ramp=[#ffffff, #453422,  #f0f0f0, #aaaaaa],\n                                cmap_type='values')\n    '''\n    geo.publish_style(layer_name='geoserver_layer_name', style_name='raster_file_name', workspace='demo')\n\nFor generating the style for ``classified raster``, you can pass the another parameter called ``cmap_type='values'`` as,\n\n\n.. code-block:: python\n\n    geo.create_coveragestyle(raster_path=r'path\\to\\raster\\file.tiff', style_name='style_1', workspace='demo', color_ramp='RdYiGn', cmap_type='values')\n\n\n.. list-table:: Options for ``create_coveragestyle``\n    :widths: 15 15 15 55\n    :header-rows: 1\n\n    * - Option\n      - Type\n      - Default\n      - Description\n\n    * - style_name\n      - string\n      - file_name\n      - This is optional field. If you don't pass the style_name parameter, then it will take the raster file name as the default name of style in geoserver\n\n    * - raster_path\n      - path\n      - None\n      - path to the raster file (Required)\n\n    * - workspace\n      - string\n      - None\n      - The name of the workspace. Optional field. It will take the default workspace of geoserver if nothing is provided\n\n    * - color_ramp\n      - string, list, dict\n      - RdYiGn\n      - The color ramp name. The name of the color ramp can be found here in `matplotlib colormaps <https://matplotlib.org/3.3.0/tutorials/colors/colormaps.html>`_\n\n    * - overwrite\n      - boolean\n      - False\n      - For overwriting the previous style file in geoserver\n\n\nCreating feature styles\n-----------------------\n\n**WARNING:** As of version 2.9.0, the required dependency ``gdal``, ``matplotlib`` and ``seaborn`` was converted into an optional dependency. Fresh installations of this library will require that you then install ``gdal``, ``matplotlib`` and ``seaborn`` yourself with ``pip install gdal matplotlib seaborn``.\n\nIt is used for creating the style for ``point``, ``line`` and ``polygon`` dynamically. It currently supports three different types of feature styles:\n\n1. ``Outline featurestyle``: For creating the style which have only boundary color but not the fill style\n2. ``Catagorized featurestyle``: For creating catagorized dataset\n3. ``Classified featurestyle``: Classify the input data and style it: (For now, it only supports polygon geometry)\n\n.. code-block:: python\n\n    geo.create_outline_featurestyle(style_name='new_style', color=\"#3579b1\", geom_type='polygon', workspace='demo')\n    geo.create_catagorized_featurestyle(style_name='name_of_style', column_name='name_of_column', column_distinct_values=[1,2,3,4,5,6,7], workspace='demo')\n    geo.create_classified_featurestyle(style_name='name_of_style', column_name='name_of_column', column_distinct_values=[1,2,3,4,5,6,7], workspace='demo')\n\n.. note::\n\n    * The ``geom_type`` must be either ``point``, ``line`` or ``polygon``.\n    * The ``color_ramp`` name can be obtained from `matplotlib colormaps <https://matplotlib.org/3.3.0/tutorials/colors/colormaps.html>`_.\n\nThe options for creating categorized/classified `featurestyles` are as follows,\n\n.. list-table:: Options for ``create_catagorized_featurestyle`` and ``create_classified_featurestyle``\n    :widths: 15 15 15 55\n    :header-rows: 1\n\n    * - Option\n      - Type\n      - Default\n      - Description\n\n    * - style_name\n      - string\n      - file_name\n      - This is optional field. If you don't pass the style_name parameter, then it will take the raster file name as the default name of style in geoserver\n\n    * - column_name\n      - string\n      - None\n      - The name of the column, based on which the style will be generated\n\n    * - column_distinct_values\n      - list/array\n      - None\n      - The column distinct values based on which the style will be applied/classified. This option is only available for ``create_classified_featurestyle``\n\n    * - workspace\n      - string\n      - None\n      - The name of the workspace. Optional field. It will take the default workspace of geoserver if nothing is provided\n\n    * - color_ramp\n      - string\n      - RdYiGn\n      - The color ramp name. The name of the color ramp can be found here in `matplotlib colormaps <https://matplotlib.org/3.3.0/tutorials/colors/colormaps.html>`_\n\n    * - geom_type\n      - string\n      - polygon\n      - The geometry type, available options are ``point``, ``line`` or ``polygon``\n\n    * - outline_color\n      - color hex value\n      - '#3579b1'\n      - The outline color of the polygon/line\n\n    * - overwrite\n      - boolean\n      - False\n      - For overwriting the previous style file in geoserver\n\n\nDeletion requests examples\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: python\n\n    # delete workspace\n    geo.delete_workspace(workspace='demo')\n\n    # delete layer\n    geo.delete_layer(layer_name='agri_final_proj', workspace='demo')\n\n    # delete feature store, i.e. remove postgresql connection\n    geo.delete_featurestore(featurestore_name='ftry', workspace='demo')\n\n    # delete coveragestore, i.e. delete raster store\n    geo.delete_coveragestore(coveragestore_name='agri_final_proj', workspace='demo')\n\n    # delete style file\n    geo.delete_style(style_name='kamal2', workspace='demo')\n\n\nSome get request examples\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: python\n\n    # get geoserver version\n    version = geo.get_version()\n    print(version)\n\n    # get ststem info\n    status = geo.get_status()\n    system_status = geo.get_system_status()\n\n    # get workspace\n    workspace = geo.get_workspace(workspace='workspace_name')\n\n    # get default workspace\n    dw = geo.get_default_wokspace()\n\n    # get all the workspaces\n    workspaces = geo.get_workspaces()\n\n    # get datastore\n    datastore = geo.get_datastore(store_name='store')\n\n    # get all the datastores\n    datastores = geo.get_datastores()\n\n    # get coveragestore\n    cs = geo.get_coveragestore(coveragestore_name='cs')\n\n    # get all the coveragestores\n    css = geo.get_coveragestores()\n\n    # get layer\n    layer = geo.get_layer(layer_name='layer_name')\n\n    # get all the layers\n    layers = geo.get_layers()\n\n    # get layergroup\n    layergroup = geo.get_layergroup('layergroup_name')\n\n    # get all the layers\n    layergroups = geo.get_layergroups()\n\n    # get style\n    style = geo.get_style(style_name='style_name')\n\n    # get all the styles\n    styles = geo.get_styles()\n\n    # get featuretypes\n    featuretypes = geo.get_featuretypes(store_name='store_name')\n\n    # get feature attribute\n    fa = geo.get_feature_attribute(feature_type_name='ftn', workspace='ws', store_name='sn')\n\n    # get feature store\n    fs = geo.get_featurestore(store_name='sn', workspace='ws')\n\n\nSpecial functions\n^^^^^^^^^^^^^^^^^\n\n.. code-block:: python\n\n    # Reloads the GeoServer catalog and configuration from disk. This operation is used in cases where an external tool has modified the on-disk configuration. This operation will also force GeoServer to drop any internal caches and reconnect to all data stores.\n    geo.reload()\n\n    # Resets all store, raster, and schema caches. This operation is used to force GeoServer to drop all caches and store connections and reconnect to each of them the next time they are needed by a request. This is useful in case the stores themselves cache some information about the data structures they manage that may have changed in the meantime.\n    geo.reset()\n\n    # set default workspace\n    geo.set_default_workspace(workspace='workspace_name')\n\n\n\nGlobal parameters for most functions\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe following parameters are common to most functions/methods:\n\n* ``workspace``: If workspace is not provided, the function will take the ``default`` workspace.\n* ``overwrite``: This parameter takes only the boolean value. In most of the create method, the ``overwrite`` parameter is available. The default value is ``False``. But if you set it to True, the method will be in update mode.\n"
  },
  {
    "path": "docs/source/index.rst",
    "content": ".. geoserver-rest documentation master file, created by\n   sphinx-quickstart on Wed Mar 10 14:46:38 2021.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to geoserver-rest documentation!\n========================================\n\n\n.. toctree::\n   :maxdepth: 2\n\n   about\n   installation\n   how_to_use\n   advanced_uses\n   change_log\n   license\n   contribution\n   acknowledgements\n   geo\n\nCheckout the video tutorial on geoserver-rest below,\n\n.. raw:: html\n\n    <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/nXvzmbGukeE\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n"
  },
  {
    "path": "docs/source/installation.rst",
    "content": "Installation\n=============\n\n.. warning::\n    As of version 2.9.0, the required dependency ``gdal``, ``matplotlib`` and ``seaborn`` was converted into an optional dependency. Fresh installations of this library will require that you then install ``gdal``, ``matplotlib`` and ``seaborn`` yourself with ``pip install gdal matplotlib seaborn``.\n\n\nConda installation\n^^^^^^^^^^^^^^^^^^\n\nThe ``geoserver-rest`` can be installed from either ``conda-forge`` channel as below:\n\n.. code-block:: shell\n\n    $ conda install -c conda-forge geoserver-rest\n\nPip installation\n^^^^^^^^^^^^^^^^\n\nThe ``geoserver-rest`` library can be installed using ``pip`` as below:\n\n.. code-block:: shell\n\n    $ pip install geoserver-rest\n\nBut best way to get all the functationality is to install the optional dependencies as well:\n\n.. code-block:: shell\n\n    $ pip install geoserver-rest[all]\n\nIf you want to install the geoserver-rest library with the optional dependencies (this will be useful if you are planning to create dynamic style files based on your dataset. Explore ``create_coveragestyle``, ``upload_style`` etc functions), you need to install the following dependencies first:\n\n* `GDAL <https://gdal.org/>`_\n* `matplotlib <https://matplotlib.org/>`_\n* `seaborn <https://seaborn.pydata.org/>`_\n\n\nDependencies installation in Windows\n------------------------------------\n\n.. warning::\n    As of March 2022, ``pipwin`` has been deprecated and is no longer maintained. Do not use this method.\n\nFor Windows, the ``gdal`` dependency can be complex to install. There are a handful of ways to install ``gdal`` in Windows.\n\nOne way is install the wheel directly from the `Geospatial library wheels for Python Windows <https://github.com/cgohlke/geospatial-wheels>`_ releases page. Be sure to select the wheel for your system from the latest release and install it using pip install command:\n\n.. code-block:: shell\n\n    # For Python3.10 on Windows 64-bit systems\n    $ pip.exe install https://github.com/cgohlke/geospatial-wheels/releases/download/<release_version>/GDAL-3.7.1-cp310-cp310-win_amd64.whl\n    $ pip.exe install seaborn matplotlib\n\nAnother way is to use the GDAL network installer binary package available at: `OSGeo4W <https://trac.osgeo.org/osgeo4w/>`_.\n\n\nmacOS installation\n------------------\n\nFor macOS, we suggest using the `homebrew` package manager to install ``gdal``. Once ``homebrew`` is installed, ``gdal`` can be installed using following method:\n\n.. code-block:: shell\n\n    $ brew update\n    $ brew install gdal\n    $ pip3 install pygdal==\"$(gdalinfo --version | awk '{print $2}' | sed s'/.$//')\"\n\nLinux installation\n------------------\n\nFor Ubuntu specifically, we suggest installing ``gdal`` from the ``ubuntugis`` PPA:\n\n.. code-block:: shell\n\n    $ sudo add-apt-repository ppa:ubuntugis/ppa\n    $ sudo apt update -y\n    $ sudo apt upgrade -y\n    $ sudo apt install gdal-bin libgdal-dev\n\nFor other versions of Linux, simply use your package manager to install ``gdal``.\n\n.. code-block:: shell\n\n    # Debian, Mint, etc.\n    $ sudo apt install gdal-bin libgdal-dev\n    # Fedora, RHEL, etc.\n    $ sudo yum install gdal gdal-devel\n    # Arch, Manjaro, etc.\n    $ sudo pacman -S gdal\n    # Void Linux\n    $ sudo xbps-install -S libgdal libgdal-devel\n\nNow the ``pygdal`` and ``geoserver-rest`` libraries can be installed using ``pip``:\n\n.. code-block:: shell\n\n    $ pip install pygdal==\"$(gdal-config --version).*\"\n    $ pip install geoserver-rest\n"
  },
  {
    "path": "docs/source/license.rst",
    "content": "License\n=========\n\nMIT License\n^^^^^^^^^^^^\n\n\nCopyright (c) 2020, Geoinformatics Center, Asian Institute of Technology\n\nCopyright (c) 2020, Tek Kshetri\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "geo/Calculation_gdal.py",
    "content": "import os\n\ntry:\n    from osgeo import gdal  # noqa\nexcept ImportError:\n    import gdal  # noqa\nexcept ImportError:\n    raise ImportError(\"Package `gdal` is required to run this function. Install it with `pip install gdal`.\")\n\n\ndef raster_value(path: str) -> dict:\n    file = os.path.basename(path)\n    file_format = os.path.splitext(file)[1]\n    file_name = file.split(\".\")[0]\n    valid_extension = [\".tif\", \".tiff\", \".gtiff\"]\n\n    if file_format.lower() in valid_extension:\n        try:\n            gtif = gdal.Open(path)\n            srcband = gtif.GetRasterBand(1)\n            srcband.ComputeStatistics(0)\n            n = srcband.GetMaximum() - srcband.GetMinimum() + 1\n            n = int(n)\n            min_value = srcband.GetMinimum()\n            max_value = srcband.GetMaximum()\n            result = {\n                \"N\": n,\n                \"min\": min_value,\n                \"max\": max_value,\n                \"file_name\": file_name,\n            }\n            return result\n        except Exception as error:\n            print(\"An error occured when computing band statistics: \", error)\n            raise error\n\n    else:\n        print(\"sorry, file format is incorrect\")\n"
  },
  {
    "path": "geo/Geoserver.py",
    "content": "# inbuilt libraries\nimport os\nfrom typing import List, Optional, Set, Union, Dict, Iterable, Any\nfrom pathlib import Path\n\n# third-party libraries\nimport requests\nfrom xmltodict import parse, unparse\n\n# custom functions\nfrom .supports import prepare_zip_file, is_valid_xml, is_surrounded_by_quotes\n\n\ndef _parse_request_options(request_options: Dict[str, Any]):\n    \"\"\"\n    Parse request options.\n\n    Parameters\n    ----------\n    request_options : dict\n        The request options to parse.\n\n    Returns\n    -------\n    dict\n        The parsed request options.\n    \"\"\"\n    return request_options if request_options is not None else {}\n\n\n# Custom exceptions.\nclass GeoserverException(Exception):\n    \"\"\"\n    Custom exception for Geoserver errors.\n\n    Parameters\n    ----------\n    status : int\n        The status code of the error.\n    message : str\n        The error message.\n    \"\"\"\n\n    def __init__(self, status, message):\n        self.status = status\n        self.message = message\n        super().__init__(f\"Status : {self.status} - {self.message}\")\n\n\n# call back class for reading the data\nclass DataProvider:\n    \"\"\"\n    Data provider for reading data.\n\n    Parameters\n    ----------\n    data : str\n        The data to be read.\n    \"\"\"\n\n    def __init__(self, data):\n        self.data = data\n        self.finished = False\n\n    def read_cb(self, size):\n        \"\"\"\n        Read callback.\n\n        Parameters\n        ----------\n        size : int\n            The size of the data to read.\n\n        Returns\n        -------\n        str\n            The read data.\n        \"\"\"\n        assert len(self.data) <= size\n        if not self.finished:\n            self.finished = True\n            return self.data\n        else:\n            # Nothing more to read\n            return \"\"\n\n\n# callback class for reading the files\nclass FileReader:\n    \"\"\"\n    File reader for reading files.\n\n    Parameters\n    ----------\n    fp : file object\n        The file object to read from.\n    \"\"\"\n\n    def __init__(self, fp):\n        self.fp = fp\n\n    def read_callback(self, size):\n        \"\"\"\n        Read callback.\n\n        Parameters\n        ----------\n        size : int\n            The size of the data to read.\n\n        Returns\n        -------\n        str\n            The read data.\n        \"\"\"\n        return self.fp.read(size)\n\n\nclass Geoserver:\n    \"\"\"\n    Geoserver class to interact with GeoServer REST API.\n\n    Attributes\n    ----------\n    service_url : str\n        The URL for the GeoServer instance.\n    username : str\n        Login name for session.\n    password: str\n        Password for session.\n    request_options : dict\n        Additional parameters to be sent with each request.\n    \"\"\"\n\n    def __init__(\n        self,\n        service_url: str = \"http://localhost:8080/geoserver\",  # default deployment url during installation\n        username: str = \"admin\",  # default username during geoserver installation\n        password: str = \"geoserver\",  # default password during geoserver installation\n        request_options: Dict[\n            str, Any\n        ] = None,  # additional parameters to be sent with each request\n    ):\n        self.service_url = service_url\n        self.username = username\n        self.password = password\n        self.request_options = request_options if request_options is not None else {}\n\n    def _requests(self, method: str, url: str, **kwargs) -> requests.Response:\n        \"\"\"\n        Convenience wrapper to the requests library which automatically handles the authentication, as well as additional options to be passed to each request.\n\n        Parameters\n        ----------\n        method : str\n            Which method to use (`get`, `post`, `put`, `delete`)\n        url : str\n            URL to which to make the request\n        kwargs : dict\n            Additional arguments to pass to the request.\n\n        Returns\n        -------\n        requests.Response\n            The response object.\n        \"\"\"\n\n        if method.lower() == \"post\":\n            return requests.post(\n                url,\n                auth=(self.username, self.password),\n                **kwargs,\n                **self.request_options,\n            )\n        elif method.lower() == \"get\":\n            return requests.get(\n                url,\n                auth=(self.username, self.password),\n                **kwargs,\n                **self.request_options,\n            )\n        elif method.lower() == \"put\":\n            return requests.put(\n                url,\n                auth=(self.username, self.password),\n                **kwargs,\n                **self.request_options,\n            )\n        elif method.lower() == \"delete\":\n            return requests.delete(\n                url,\n                auth=(self.username, self.password),\n                **kwargs,\n                **self.request_options,\n            )\n        else:\n            raise Exception(\"unsupported http method name.\")\n\n    # _______________________________________________________________________________________________\n    #\n    #       GEOSERVER AND SERVER SPECIFIC METHODS\n    # _______________________________________________________________________________________________\n    #\n\n    def get_manifest(self):\n        \"\"\"\n        Returns the manifest of the GeoServer. The manifest is a JSON of all the loaded JARs on the GeoServer server.\n\n        Returns\n        -------\n        dict\n            The manifest of the GeoServer.\n        \"\"\"\n        url = \"{}/rest/about/manifest.json\".format(self.service_url)\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_version(self):\n        \"\"\"\n        Returns the version of the GeoServer as JSON. It contains only the details of the high level components: GeoServer, GeoTools, and GeoWebCache.\n\n        Returns\n        -------\n        dict\n            The version information of the GeoServer.\n        \"\"\"\n        url = \"{}/rest/about/version.json\".format(self.service_url)\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_status(self):\n        \"\"\"\n        Returns the status of the GeoServer. It shows the status details of all installed and configured modules.\n\n        Returns\n        -------\n        dict\n            The status of the GeoServer.\n        \"\"\"\n        url = \"{}/rest/about/status.json\".format(self.service_url)\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_system_status(self):\n        \"\"\"\n        Returns the system status of the GeoServer. It returns a list of system-level information. Major operating systems (Linux, Windows, and MacOS) are supported out of the box.\n\n        Returns\n        -------\n        dict\n            The system status of the GeoServer.\n        \"\"\"\n        url = \"{}/rest/about/system-status.json\".format(self.service_url)\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def reload(self):\n        \"\"\"\n        Reloads the GeoServer catalog and configuration from disk.\n\n        This operation is used in cases where an external tool has modified the on-disk configuration. This operation will also force GeoServer to drop any internal caches and reconnect to all data stores.\n\n        Returns\n        -------\n        str\n            The status code of the reload operation.\n        \"\"\"\n        url = \"{}/rest/reload\".format(self.service_url)\n        r = self._requests(\"post\", url)\n        if r.status_code == 200:\n            return \"Status code: {}\".format(r.status_code)\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def reset(self):\n        \"\"\"\n        Resets all store, raster, and schema caches. This operation is used to force GeoServer to drop all caches and store connections and reconnect to each of them the next time they are needed by a request. This is useful in case the stores themselves cache some information about the data structures they manage that may have changed in the meantime.\n\n        Returns\n        -------\n        str\n            The status code of the reset operation.\n        \"\"\"\n        url = \"{}/rest/reset\".format(self.service_url)\n        r = self._requests(\"post\", url)\n        if r.status_code == 200:\n            return \"Status code: {}\".format(r.status_code)\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    # _______________________________________________________________________________________________\n    #\n    #      WORKSPACES\n    # _______________________________________________________________________________________________\n    #\n\n    def get_default_workspace(self):\n        \"\"\"\n        Returns the default workspace.\n\n        Returns\n        -------\n        dict\n            The default workspace.\n        \"\"\"\n        url = \"{}/rest/workspaces/default\".format(self.service_url)\n        r = self._requests(\"get\", url)\n\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_workspace(self, workspace):\n        \"\"\"\n        Get the name of a workspace if it exists.\n\n        Parameters\n        ----------\n        workspace : str\n            The name of the workspace.\n\n        Returns\n        -------\n        dict\n            The workspace information.\n        \"\"\"\n        url = \"{}/rest/workspaces/{}.json\".format(self.service_url, workspace)\n        r = self._requests(\"get\", url, params={\"recurse\": \"true\"})\n\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_workspaces(self):\n        \"\"\"\n        Returns all the workspaces.\n\n        Returns\n        -------\n        dict\n            All the workspaces.\n        \"\"\"\n        url = \"{}/rest/workspaces\".format(self.service_url)\n        r = self._requests(\"get\", url)\n\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def set_default_workspace(self, workspace: str):\n        \"\"\"\n        Set the default workspace.\n\n        Parameters\n        ----------\n        workspace : str\n            The name of the workspace to set as default.\n\n        Returns\n        -------\n        str\n            The status code of the operation.\n        \"\"\"\n        url = \"{}/rest/workspaces/default\".format(self.service_url)\n        data = \"<workspace><name>{}</name></workspace>\".format(workspace)\n\n        r = self._requests(\"put\", url, data=data, headers={\"content-type\": \"text/xml\"})\n\n        if r.status_code == 200:\n            return \"Status code: {}, default workspace {} set!\".format(\n                r.status_code, workspace\n            )\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_workspace(self, workspace: str):\n        \"\"\"\n        Create a new workspace in GeoServer. The GeoServer workspace URL will be the same as the name of the workspace.\n\n        Parameters\n        ----------\n        workspace : str\n            The name of the workspace to create.\n\n        Returns\n        -------\n        str\n            The status code and message of the operation.\n        \"\"\"\n        url = \"{}/rest/workspaces\".format(self.service_url)\n        data = \"<workspace><name>{}</name></workspace>\".format(workspace)\n        headers = {\"content-type\": \"text/xml\"}\n        r = self._requests(\"post\", url, data=data, headers=headers)\n\n        if r.status_code == 201:\n            return \"{} Workspace {} created!\".format(r.status_code, workspace)\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def delete_workspace(self, workspace: str):\n        \"\"\"\n        Delete a workspace.\n\n        Parameters\n        ----------\n        workspace : str\n            The name of the workspace to delete.\n\n        Returns\n        -------\n        str\n            The status code and message of the operation.\n        \"\"\"\n        payload = {\"recurse\": \"true\"}\n        url = \"{}/rest/workspaces/{}\".format(self.service_url, workspace)\n        r = self._requests(\"delete\", url, params=payload)\n\n        if r.status_code == 200:\n            return \"Status code: {}, delete workspace\".format(r.status_code)\n\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    # _______________________________________________________________________________________________\n    #\n    #       DATASTORES\n    # _______________________________________________________________________________________________\n    #\n\n    def get_datastore(self, store_name: str, workspace: Optional[str] = None):\n        \"\"\"\n        Return the data store in a given workspace. If workspace is not provided, it will take the default workspace.\n\n        Parameters\n        ----------\n        store_name : str\n            The name of the data store.\n        workspace : str, optional\n            The name of the workspace.\n\n        Returns\n        -------\n        dict\n            The data store information.\n        \"\"\"\n        if workspace is None:\n            workspace = \"default\"\n\n        url = \"{}/rest/workspaces/{}/datastores/{}\".format(\n            self.service_url, workspace, store_name\n        )\n\n        r = self._requests(\"get\", url)\n\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_datastores(self, workspace: Optional[str] = None):\n        \"\"\"\n        List all data stores in a workspace. If workspace is not provided, it will list all the datastores inside the default workspace.\n\n        Parameters\n        ----------\n        workspace : str, optional\n            The name of the workspace.\n\n        Returns\n        -------\n        dict\n            The list of data stores.\n        \"\"\"\n        if workspace is None:\n            workspace = \"default\"\n\n        url = \"{}/rest/workspaces/{}/datastores.json\".format(\n            self.service_url, workspace\n        )\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    # _______________________________________________________________________________________________\n    #\n    #       COVERAGE STORES\n    # _______________________________________________________________________________________________\n    #\n\n    def get_coveragestore(\n        self, coveragestore_name: str, workspace: Optional[str] = None\n    ):\n        \"\"\"\n        Returns the store name if it exists.\n\n        Parameters\n        ----------\n        coveragestore_name : str\n            The name of the coverage store.\n        workspace : str, optional\n            The name of the workspace.\n\n        Returns\n        -------\n        dict\n            The coverage store information.\n        \"\"\"\n        payload = {\"recurse\": \"true\"}\n        if workspace is None:\n            workspace = \"default\"\n        url = \"{}/rest/workspaces/{}/coveragestores/{}.json\".format(\n            self.service_url, workspace, coveragestore_name\n        )\n        r = self._requests(method=\"get\", url=url, params=payload)\n\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_coveragestores(self, workspace: str = None):\n        \"\"\"\n        Returns all the coverage stores inside a specific workspace.\n\n        Parameters\n        ----------\n        workspace : str, optional\n            The name of the workspace.\n\n        Returns\n        -------\n        dict\n            The list of coverage stores.\n        \"\"\"\n        if workspace is None:\n            workspace = \"default\"\n\n        url = \"{}/rest/workspaces/{}/coveragestores\".format(self.service_url, workspace)\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_coverage(\n        self,\n        workspace: str,\n        coveragestore: str,\n        coverage_name: str = None,\n        navite_name: str = None,\n        coverage_title: str = None,\n    ):\n        \"\"\"\n        Create a coverage in a coveragestore.The coveragestore must already exist.\n\n        Parameters\n        ----------\n        workspace: The workspace name\n        coveragestore: The coveragestore name\n        coverage_name: The coverage name\n        native_Name: The coverage's source name\n        coverage_title: The coverage title\n\n        Returns\n        -------\n        str\n            The coverage name.\n        \"\"\"\n\n        if not workspace:\n            raise ValueError(\"Workspace is required\")\n        if not coveragestore:\n            raise ValueError(\"Coveragestore is required\")\n\n        navite_name = navite_name or coveragestore\n        coverage_name = coverage_name or navite_name\n        coverage_title = coverage_title.replace(\" \", \"_\") if coverage_title else None\n\n        body = {\n            \"coverage\": {\n                \"name\": coverage_name,\n                \"nativeName\": navite_name,\n                \"title\": coverage_title,\n                \"store\": {\"name\": coveragestore},\n            }\n        }\n\n        url = f\"{self.service_url}/rest/workspaces/{workspace}/coverages\"\n        content_type = \"application/json\"\n        r = self._requests(\n            method=\"post\", url=url, json=body, headers={\"content-type\": content_type}\n        )\n\n        if r.status_code == 201:\n            return r.text\n        raise GeoserverException(r.status_code, r.content)\n\n    def create_coveragestore(\n        self,\n        path,\n        workspace: Optional[str] = None,\n        layer_name: Optional[str] = None,\n        file_type: str = \"GeoTIFF\",\n        content_type: str = \"image/tiff\",\n        method: str = \"file\",\n    ):\n        \"\"\"\n        Creates the coverage store; Data will be uploaded to the server.\n\n        Parameters\n        ----------\n        path : str\n            The path to the file.\n        workspace : str, optional\n            The name of the workspace.\n        layer_name : str, optional\n            The name of the coverage store. If not provided, parsed from the file name.\n        file_type : str\n            The type of the file.\n        content_type : str\n            The content type of the file.\n        method : str\n            file | url | external | remote\n\n        Returns\n        -------\n        dict\n            The response from the server.\n\n        Notes\n        -----\n        the path to the file and file_type indicating it is a geotiff, arcgrid or other raster type\n        \"\"\"\n        if path is None:\n            raise Exception(\"You must provide the full path to the raster\")\n\n        if workspace is None:\n            workspace = \"default\"\n\n        if layer_name is None:\n            layer_name = os.path.basename(path)\n            f = layer_name.split(\".\")\n            if len(f) > 0:\n                layer_name = f[0]\n\n        file_type = file_type.lower()\n        if file_type == \"netcdf\":\n            # files such as netcdf contain multiple layers, which means a single coverage name cannot be specified.\n            url = \"{0}/rest/workspaces/{1}/coveragestores/{2}/{3}.{4}\".format(\n                self.service_url, workspace, layer_name, method, file_type\n            )\n        else:\n            url = \"{0}/rest/workspaces/{1}/coveragestores/{2}/{3}.{4}?coverageName={2}\".format(\n                self.service_url, workspace, layer_name, method, file_type\n            )\n\n        if method == \"file\":\n            headers = {\"content-type\": content_type, \"Accept\": \"application/json\"}\n            with open(path, \"rb\") as f:\n                r = self._requests(method=\"put\", url=url, data=f, headers=headers)\n        else:\n            headers = {\"content-type\": \"text/plain\", \"Accept\": \"application/json\"}\n            r = self._requests(method=\"put\", url=url, data=path, headers=headers)\n\n        if r.status_code == 201:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def publish_time_dimension_to_coveragestore(\n        self,\n        store_name: Optional[str] = None,\n        workspace: Optional[str] = None,\n        presentation: Optional[str] = \"LIST\",\n        units: Optional[str] = \"ISO8601\",\n        default_value: Optional[str] = \"MINIMUM\",\n        content_type: str = \"application/xml; charset=UTF-8\",\n    ):\n        \"\"\"\n        Create time dimension in coverage store to publish time series in GeoServer.\n\n        Parameters\n        ----------\n        store_name : str, optional\n            The name of the coverage store.\n        workspace : str, optional\n            The name of the workspace.\n        presentation : str, optional\n            The presentation style.\n        units : str, optional\n            The units of the time dimension.\n        default_value : str, optional\n            The default value of the time dimension.\n        content_type : str\n            The content type of the request.\n\n        Returns\n        -------\n        dict\n            The response from the server.\n\n        Notes\n        -----\n        More about time support in geoserver WMS you can read here:\n        https://docs.geoserver.org/master/en/user/services/wms/time.html\n        \"\"\"\n        url = \"{0}/rest/workspaces/{1}/coveragestores/{2}/coverages/{2}\".format(\n            self.service_url, workspace, store_name\n        )\n\n        headers = {\"content-type\": content_type}\n\n        time_dimension_data = (\n            \"<coverage>\"\n            \"<enabled>true</enabled>\"\n            \"<metadata>\"\n            \"<entry key='time'>\"\n            \"<dimensionInfo>\"\n            \"<enabled>true</enabled>\"\n            \"<presentation>{}</presentation>\"\n            \"<units>{}</units>\"\n            \"<defaultValue>\"\n            \"<strategy>{}</strategy>\"\n            \"</defaultValue>\"\n            \"</dimensionInfo>\"\n            \"</entry>\"\n            \"</metadata>\"\n            \"</coverage>\".format(presentation, units, default_value)\n        )\n\n        r = self._requests(\n            method=\"put\", url=url, data=time_dimension_data, headers=headers\n        )\n        if r.status_code in [200, 201]:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    # _______________________________________________________________________________________________\n    #\n    #       LAYERS\n    # _______________________________________________________________________________________________\n    #\n\n    def get_layer(self, layer_name: str, workspace: Optional[str] = None):\n        \"\"\"\n        Returns the layer by layer name.\n\n        Parameters\n        ----------\n        layer_name : str\n            The name of the layer.\n        workspace : str, optional\n            The name of the workspace.\n\n        Returns\n        -------\n        dict\n            The layer information.\n        \"\"\"\n        url = \"{}/rest/layers/{}\".format(self.service_url, layer_name)\n        if workspace is not None:\n            url = \"{}/rest/workspaces/{}/layers/{}\".format(\n                self.service_url, workspace, layer_name\n            )\n\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_layers(self, workspace: Optional[str] = None):\n        \"\"\"\n        Get all the layers from GeoServer. If workspace is None, it will list all the layers from GeoServer.\n\n        Parameters\n        ----------\n        workspace : str, optional\n            The name of the workspace.\n\n        Returns\n        -------\n        dict\n            The list of layers.\n        \"\"\"\n        url = \"{}/rest/layers\".format(self.service_url)\n\n        if workspace is not None:\n            url = \"{}/rest/workspaces/{}/layers\".format(self.service_url, workspace)\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def delete_layer(self, layer_name: str, workspace: Optional[str] = None):\n        \"\"\"\n        Delete a layer.\n\n        Parameters\n        ----------\n        layer_name : str\n            The name of the layer to delete.\n        workspace : str, optional\n            The name of the workspace.\n\n        Returns\n        -------\n        str\n            The status code and message of the operation.\n        \"\"\"\n        payload = {\"recurse\": \"true\"}\n        url = \"{}/rest/workspaces/{}/layers/{}\".format(\n            self.service_url, workspace, layer_name\n        )\n        if workspace is None:\n            url = \"{}/rest/layers/{}\".format(self.service_url, layer_name)\n\n        r = self._requests(method=\"delete\", url=url, params=payload)\n        if r.status_code == 200:\n            return \"Status code: {}, delete layer\".format(r.status_code)\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    # _______________________________________________________________________________________________\n    #\n    #       LAYER GROUPS\n    # _______________________________________________________________________________________________\n    #\n\n    def get_layergroups(self, workspace: Optional[str] = None):\n        \"\"\"\n        Returns all the layer groups from GeoServer. If workspace is None, it will list all the layer groups from GeoServer.\n\n        Parameters\n        ----------\n        workspace : str, optional\n            The name of the workspace.\n\n        Returns\n        -------\n        dict\n            The list of layer groups.\n\n        Notes\n        -----\n        If workspace is None, it will list all the layer groups from geoserver.\n        \"\"\"\n        url = \"{}/rest/layergroups\".format(self.service_url)\n\n        if workspace is not None:\n            url = \"{}/rest/workspaces/{}/layergroups\".format(\n                self.service_url, workspace\n            )\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_layergroup(self, layer_name: str, workspace: Optional[str] = None):\n        \"\"\"\n        Returns the layer group by layer group name.\n\n        Parameters\n        ----------\n        layer_name : str\n            The name of the layer group.\n        workspace : str, optional\n            The name of the workspace.\n\n        Returns\n        -------\n        dict\n            The layer group information.\n        \"\"\"\n        url = \"{}/rest/layergroups/{}\".format(self.service_url, layer_name)\n        if workspace is not None:\n            url = \"{}/rest/workspaces/{}/layergroups/{}\".format(\n                self.service_url, workspace, layer_name\n            )\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_layergroup(\n        self,\n        name: str = \"geoserver-rest-layergroup\",\n        mode: str = \"single\",\n        title: str = \"geoserver-rest layer group\",\n        abstract_text: str = \"A new layergroup created with geoserver-rest python package\",\n        layers: List[str] = [],\n        workspace: Optional[str] = None,\n        formats: str = \"html\",\n        metadata: List[dict] = [],\n        keywords: List[str] = [],\n    ) -> str:\n        \"\"\"\n        Creates the Layergroup.\n\n        Parameters\n        ----------\n        name : str\n            The name of the layer group.\n        mode : str\n            The mode of the layer group.\n        title : str\n            The title of the layer group.\n        abstract_text : str\n            The abstract text of the layer group.\n        layers : list\n            The list of layers in the layer group.\n        workspace : str, optional\n            The name of the workspace.\n        formats : str, optional\n            The format of the layer group.\n        metadata : list, optional\n            The metadata of the layer group.\n        keywords : list, optional\n            The keywords of the layer group.\n\n        Returns\n        -------\n        str\n            The URL of the created layer group.\n\n        Notes\n        -----\n        title is a human readable text for the layergroup\n        abstract_text is a long text, like a brief info about the layergroup\n        workspace is Optional(Global Layergroups don't need workspace).A layergroup can exist without a workspace.\n        \"\"\"\n        assert isinstance(name, str), \"Name must be of type String:''\"\n        assert isinstance(mode, str), \"Mode must be of type String:''\"\n        assert isinstance(title, str), \"Title must be of type String:''\"\n        assert isinstance(abstract_text, str), \"Abstract text must be of type String:''\"\n        assert isinstance(formats, str), \"Format must be of type String:''\"\n        assert isinstance(\n            metadata, list\n        ), \"Metadata must be of type List of dict:[{'about':'geoserver rest data metadata','content_url':'link to content url'}]\"\n        assert isinstance(\n            keywords, list\n        ), \"Keywords must be of type List:['keyword1','keyword2'...]\"\n        assert isinstance(\n            layers, list\n        ), \"Layers must be of type List:['layer1','layer2'...]\"\n\n        if workspace:\n            assert isinstance(workspace, str), \"Workspace must be of type String:''\"\n            # check if the workspace is valid in GeoServer\n            if self.get_workspace(workspace) is None:\n                raise Exception(\"Workspace is not valid in GeoServer Instance\")\n\n        supported_modes: Set = {\n            \"single\",\n            \"opaque\",\n            \"named\",\n            \"container\",\n            \"eo\",\n        }\n        supported_formats: Set = {\"html\", \"json\", \"xml\"}\n\n        if mode.lower() != \"single\" and mode.lower() not in supported_modes:\n            raise Exception(\n                f\"Mode not supported. Acceptable modes are : {supported_modes}\"\n            )\n\n        if formats.lower() != \"html\" and formats.lower() not in supported_formats:\n            raise Exception(\n                f\"Format not supported. Acceptable formats are : {supported_formats}\"\n            )\n\n        # check if it already exist in GeoServer\n        try:\n            existing_layergroup = self.get_layergroup(name, workspace=workspace)\n        except GeoserverException:\n            existing_layergroup = None\n\n        if existing_layergroup is not None:\n            raise Exception(f\"Layergroup: {name} already exist in GeoServer instance\")\n\n        if len(layers) == 0:\n            raise Exception(\"No layer provided!\")\n        else:\n            for layer in layers:\n                # check if it is valid in geoserver\n                try:\n                    # Layer check\n                    self.get_layer(\n                        layer_name=layer,\n                        workspace=workspace if workspace is not None else None,\n                    )\n                except GeoserverException:\n                    try:\n                        # Layer group check\n                        self.get_layergroup(\n                            layer_name=layer,\n                            workspace=workspace if workspace is not None else None,\n                        )\n                    except GeoserverException:\n                        raise Exception(\n                            f\"Layer: {layer} is not a valid layer in the GeoServer instance\"\n                        )\n\n        skeleton = \"\"\n\n        if workspace:\n            skeleton += f\"<workspace><name>{workspace}</name></workspace>\"\n        # metadata structure = [{about:\"\",content_url:\"\"},{...}]\n        metadata_xml_list = []\n\n        if len(metadata) >= 1:\n            for meta in metadata:\n                metadata_about = meta.get(\"about\")\n                metadata_content_url = meta.get(\"content_url\")\n                metadata_xml_list.append(\n                    f\"\"\"\n                            <metadataLink>\n                                <type>text/plain</type>\n                                <about>{metadata_about}</about>\n                                <metadataType>ISO19115:2003</metadataType>\n                                <content>{metadata_content_url}</content>\n                            </metadataLink>\n                            \"\"\"\n                )\n\n            metadata_xml = f\"<metadataLinks>{''.join(['{}'] * len(metadata_xml_list)).format(*metadata_xml_list)}</metadataLinks>\"\n            skeleton += metadata_xml\n        layers_xml_list: List[str] = []\n\n        for layer in layers:\n            published_type = \"layer\"\n            try:\n                # Layer check\n                self.get_layer(\n                    layer_name=layer,\n                    workspace=workspace if workspace is not None else None,\n                )\n            except GeoserverException:  # It's a layer group\n                published_type = \"layerGroup\"\n\n            layers_xml_list.append(\n                f\"\"\"<published type=\"{published_type}\">\n                            <name>{layer}</name>\n                            <link>{self.service_url}/layers/{layer}.xml</link>\n                        </published>\n                    \"\"\"\n            )\n\n        layers_xml: str = (\n            f\"<publishables>{''.join(['{}'] * len(layers)).format(*layers_xml_list)}</publishables>\"\n        )\n        skeleton += layers_xml\n\n        if len(keywords) >= 1:\n            keyword_xml_list: List[str] = [\n                f\"<keyword>{keyword}</keyword>\" for keyword in keywords\n            ]\n            keywords_xml: str = (\n                f\"<keywords>{''.join(['{}'] * len(keywords)).format(*keyword_xml_list)}</keywords>\"\n            )\n            skeleton += keywords_xml\n\n        data = f\"\"\"\n                    <layerGroup>\n\n                        <name>{name}</name>\n                        <mode>{mode}</mode>\n                        <title>{title}</title>\n                        <abstractTxt>{abstract_text}</abstractTxt>\n                        {skeleton}\n                    </layerGroup>\n                \"\"\"\n\n        url = f\"{self.service_url}/rest/layergroups/\"\n\n        r = self._requests(\n            method=\"post\", url=url, data=data, headers={\"content-type\": \"text/xml\"}\n        )\n        if r.status_code == 201:\n            layergroup_url = f\"{self.service_url}/rest/layergroups/{name}.{formats}\"\n            return f\"layergroup created successfully! Layergroup link: {layergroup_url}\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def update_layergroup(\n        self,\n        layergroup_name,\n        title: Optional[str] = None,\n        abstract_text: Optional[str] = None,\n        formats: str = \"html\",\n        metadata: List[dict] = [],\n        keywords: List[str] = [],\n    ) -> str:\n        \"\"\"\n        Updates a Layergroup.\n\n        Parameters\n        ----------\n        layergroup_name: str\n            The name of the layergroup to update.\n        title : str, optional\n            The new title for the layergroup.\n        abstract_text : str, optional\n            The new abstract text for the layergroup.\n        formats : str, optional\n            The format of the response. Default is \"html\".\n        metadata : list of dict, optional\n            List of metadata entries where each entry is a dictionary with \"about\" and \"content_url\" keys.\n        keywords : list of str, optional\n            List of keywords associated with the layergroup.\n\n        Returns\n        -------\n        str\n            A success message indicating that the layergroup was updated.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue updating the layergroup.\n        \"\"\"\n        # check if layergroup is valid in Geoserver\n\n        if self.get_layergroup(layer_name=layergroup_name) is None:\n            raise Exception(\n                f\"Layer group: {layergroup_name} is not a valid layer group in the Geoserver instance\"\n            )\n        if title is not None:\n            assert isinstance(title, str), \"Title must be of type String:''\"\n        if abstract_text is not None:\n            assert isinstance(\n                abstract_text, str\n            ), \"Abstract text must be of type String:''\"\n        assert isinstance(formats, str), \"Format must be of type String:''\"\n        assert isinstance(\n            metadata, list\n        ), \"Metadata must be of type List of dict:[{'about':'geoserver rest data metadata','content_url':'lint to content url'}]\"\n        assert isinstance(\n            keywords, list\n        ), \"Keywords must be of type List:['keyword1','keyword2'...]\"\n\n        supported_formats: Set = {\"html\", \"json\", \"xml\"}\n\n        if formats.lower() != \"html\" and formats.lower() not in supported_formats:\n            raise Exception(\n                f\"Format not supported. Acceptable formats are : {supported_formats}\"\n            )\n\n        skeleton = \"\"\n\n        if title:\n            skeleton += f\"<title>{title}</title>\"\n        if abstract_text:\n            skeleton += f\"<abstractTxt>{abstract_text}</abstractTxt>\"\n\n        metadata_xml_list = []\n\n        if len(metadata) >= 1:\n            for meta in metadata:\n                metadata_about = meta.get(\"about\")\n                metadata_content_url = meta.get(\"content_url\")\n                metadata_xml_list.append(\n                    f\"\"\"\n                            <metadataLink>\n                                <type>text/plain</type>\n                                <about>{metadata_about}</about>\n                                <metadataType>ISO19115:2003</metadataType>\n                                <content>{metadata_content_url}</content>\n                            </metadataLink>\n                            \"\"\"\n                )\n\n            metadata_xml = f\"<metadataLinks>{''.join(['{}'] * len(metadata_xml_list)).format(*metadata_xml_list)}</metadataLinks>\"\n            skeleton += metadata_xml\n\n        if len(keywords) >= 1:\n            keyword_xml_list: List[str] = [\n                f\"<keyword>{keyword}</keyword>\" for keyword in keywords\n            ]\n            keywords_xml: str = (\n                f\"<keywords>{''.join(['{}'] * len(keyword_xml_list)).format(*keyword_xml_list)}</keywords>\"\n            )\n            skeleton += keywords_xml\n\n        data = f\"\"\"\n                    <layerGroup>\n                        {skeleton}\n                    </layerGroup>\n                \"\"\"\n\n        url = f\"{self.service_url}/rest/layergroups/{layergroup_name}\"\n\n        r = self._requests(\n            method=\"put\",\n            url=url,\n            data=data,\n            headers={\"content-type\": \"text/xml\", \"accept\": \"application/xml\"},\n        )\n        if r.status_code == 200:\n            layergroup_url = (\n                f\"{self.service_url}/rest/layergroups/{layergroup_name}.{formats}\"\n            )\n            return f\"layergroup updated successfully! Layergroup link: {layergroup_url}\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def delete_layergroup(\n        self, layergroup_name: str, workspace: Optional[str] = None\n    ) -> str:\n        \"\"\"\n        Delete a layer group from the geoserver and raise an exception\n        in case the layer group does not exist, or the geoserver is unavailable.\n\n        Parameters\n        ----------\n        layergroup_name: str\n            The name of the layer group to be deleted.\n        workspace: str, optional\n            The workspace the layergroup is located in.\n\n        Returns\n        -------\n        str\n            A success message indicating that the layer group was deleted.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue deleting the layergroup.\n        \"\"\"\n        # raises an exception in case the layer group doesn't exist\n        self.get_layergroup(layer_name=layergroup_name, workspace=workspace)\n\n        if workspace is None:\n            url = f\"{self.service_url}/rest/layergroups/{layergroup_name}\"\n        else:\n            url = f\"{self.service_url}/rest/workspaces/{workspace}/layergroups/{layergroup_name}\"\n\n        r = self._requests(url=url, method=\"delete\")\n        if r.status_code == 200:\n            return \"Layer group deleted successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def add_layer_to_layergroup(\n        self,\n        layer_name: str,\n        layer_workspace: str,\n        layergroup_name: str,\n        layergroup_workspace: str = None,\n    ) -> None:\n        \"\"\"\n        Add the specified layer to an existing layer group and raise an exception if\n        either the layer or layergroup doesn't exist, or the geoserver is unavailable.\n\n        Parameters\n        ----------\n        layer_name: str\n            The name of the layer.\n        layer_workspace: str\n            The workspace the layer is located in.\n        layergroup_workspace: str, optional\n            The workspace the layergroup is located in.\n        layergroup_name: str\n            The name of the layer group.\n        layergroup_workspace: str, optional\n            The workspace the layergroup is located in.\n\n        Returns\n        -------\n        None\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue adding the layer to the layergroup.\n        \"\"\"\n        layergroup_info = self.get_layergroup(\n            layer_name=layergroup_name, workspace=layergroup_workspace\n        )\n        layer_info = self.get_layer(layer_name=layer_name, workspace=layer_workspace)\n\n        # build list of existing publishables & styles\n        publishables = layergroup_info[\"layerGroup\"][\"publishables\"][\"published\"]\n        if not isinstance(publishables, list):  # only 1 layer up to now\n            publishables = [publishables]\n\n        styles = layergroup_info[\"layerGroup\"][\"styles\"][\"style\"]\n        if not isinstance(styles, list):  # only 1 layer up to now\n            styles = [styles]\n\n        # add publishable & style for the new layer\n        new_pub = {\n            \"name\": f\"{layer_workspace}:{layer_name}\",\n            \"href\": f\"{self.service_url}/rest/workspaces/{layer_workspace}/layers/{layer_name}.json\",\n        }\n        publishables.append(new_pub)\n\n        new_style = layer_info[\"layer\"][\"defaultStyle\"]\n        styles.append(new_style)\n\n        data = self._layergroup_definition_from_layers_and_styles(\n            publishables=publishables, styles=styles\n        )\n\n        if layergroup_workspace is None:\n            url = f\"{self.service_url}/rest/layergroups/{layergroup_name}\"\n        else:\n            url = f\"{self.service_url}/rest/workspaces/{layergroup_workspace}/layergroups/{layergroup_name}\"\n\n        r = self._requests(\n            method=\"put\",\n            url=url,\n            data=data,\n            headers={\"content-type\": \"text/xml\", \"accept\": \"application/xml\"},\n        )\n        if r.status_code == 200:\n            return\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def remove_layer_from_layergroup(\n        self,\n        layer_name: str,\n        layer_workspace: str,\n        layergroup_name: str,\n        layergroup_workspace: str = None,\n    ) -> None:\n        \"\"\"\n        Add remove the specified layer from an existing layer group and raise an exception if\n        either the layer or layergroup doesn't exist, or the geoserver is unavailable.\n\n        Parameters\n        ----------\n        layer_name: str\n            The name of the layer.\n        layer_workspace: str\n            The workspace the layer is located in.\n        layergroup_workspace: str, optional\n            The workspace the layergroup is located in.\n        layergroup_name: str\n            The name of the layer group.\n        layergroup_workspace: str, optional\n            The workspace the layergroup is located in.\n\n        Returns\n        -------\n        None\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue removing the layer from the layergroup.\n        \"\"\"\n        layergroup_info = self.get_layergroup(\n            layer_name=layergroup_name, workspace=layergroup_workspace\n        )\n\n        # build list of existing publishables & styles\n        publishables = layergroup_info[\"layerGroup\"][\"publishables\"][\"published\"]\n        if not isinstance(publishables, list):  # only 1 layer up to now\n            publishables = [publishables]\n\n        styles = layergroup_info[\"layerGroup\"][\"styles\"][\"style\"]\n        if not isinstance(styles, list):  # only 1 layer up to now\n            styles = [styles]\n\n        layer_to_remove = f\"{layer_workspace}:{layer_name}\"\n\n        revised_set_of_publishables_and_styles = [\n            (pub, style)\n            for (pub, style) in zip(\n                layergroup_info[\"layerGroup\"][\"publishables\"][\"published\"],\n                layergroup_info[\"layerGroup\"][\"styles\"][\"style\"],\n            )\n            if pub[\"name\"] != layer_to_remove\n        ]\n\n        revised_set_of_publishables = list(\n            map(list, zip(*revised_set_of_publishables_and_styles))\n        )[0]\n        revised_set_of_styles = list(\n            map(list, zip(*revised_set_of_publishables_and_styles))\n        )[1]\n\n        xml_payload = self._layergroup_definition_from_layers_and_styles(\n            publishables=revised_set_of_publishables, styles=revised_set_of_styles\n        )\n\n        if layergroup_workspace is None:\n            url = f\"{self.service_url}/rest/layergroups/{layergroup_name}\"\n        else:\n            url = f\"{self.service_url}/rest/workspaces/{layergroup_workspace}/layergroups/{layergroup_name}\"\n\n        r = self._requests(\n            method=\"put\",\n            url=url,\n            data=xml_payload,\n            headers={\"content-type\": \"text/xml\", \"accept\": \"application/xml\"},\n        )\n        if r.status_code == 200:\n            return\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def _layergroup_definition_from_layers_and_styles(\n        self, publishables: list, styles: list\n    ) -> str:\n        \"\"\"\n        Helper function for add_layer_to_layergroup and remove_layer_from_layergroup.\n\n        Parameters\n        ----------\n        publishables: list\n            List of publishable layers.\n        styles: list\n            List of styles associated with the publishable layers.\n\n        Returns\n        -------\n        str\n            Formatted XML request body for PUT layergroup.\n        \"\"\"\n        # the get_layergroup method may return an empty string for style;\n        # so we get the default styles for each layer with no style information in the layergroup\n        if len(styles) == 1:\n            index = [0]\n        else:\n            index = range(len(styles))\n\n        for ix, this_style, this_layer in zip(index, styles, publishables):\n            if this_style == \"\":\n                this_layer_info = self.get_layer(\n                    layer_name=this_layer[\"name\"].split(\":\")[1],\n                    workspace=this_layer[\"name\"].split(\":\")[0],\n                )\n                styles[ix] = {\n                    \"name\": this_layer_info[\"layer\"][\"defaultStyle\"][\"name\"],\n                    \"href\": this_layer_info[\"layer\"][\"defaultStyle\"][\"href\"],\n                }\n\n        # build xml structure\n        layer_skeleton = \"\"\n        style_skeleton = \"\"\n\n        for publishable in publishables:\n            layer_str = f\"\"\"\n                <published type=\"layer\">\n                    <name>{publishable['name']}</name>\n                    <link>{publishable['href']}</link>\n                </published>\n            \"\"\"\n            layer_skeleton += layer_str\n\n        for style in styles:\n            style_str = f\"\"\"\n                <style>\n                    <name>{style['name']}</name>\n                    <link>{style['href']}</link>\n                </style>\n            \"\"\"\n            style_skeleton += style_str\n\n        data = f\"\"\"\n                <layerGroup>\n                    <publishables>\n                        {layer_skeleton}\n                    </publishables>\n                    <styles>\n                        {style_skeleton}\n                    </styles>\n                </layerGroup>\n                \"\"\"\n\n        return data\n\n    # _______________________________________________________________________________________________\n    #\n    #      STYLES\n    # _______________________________________________________________________________________________\n    #\n\n    def get_style(self, style_name, workspace: Optional[str] = None):\n        \"\"\"\n        Returns the style by style name.\n\n        Parameters\n        ----------\n        style_name: str\n            The name of the style.\n        workspace: str, optional\n            The workspace the style is located in.\n\n        Returns\n        -------\n        dict\n            A dictionary representation of the style.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue retrieving the style.\n        \"\"\"\n        url = \"{}/rest/styles/{}.json\".format(self.service_url, style_name)\n        if workspace is not None:\n            url = \"{}/rest/workspaces/{}/styles/{}.json\".format(\n                self.service_url, workspace, style_name\n            )\n\n        r = self._requests(\"get\", url)\n\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_styles(self, workspace: Optional[str] = None):\n        \"\"\"\n        Returns all loaded styles from geoserver.\n\n        Parameters\n        ----------\n        workspace: str, optional\n            The workspace to filter the styles by.\n\n        Returns\n        -------\n        dict\n            A dictionary containing all the styles.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue retrieving the styles.\n        \"\"\"\n        url = \"{}/rest/styles.json\".format(self.service_url)\n\n        if workspace is not None:\n            url = \"{}/rest/workspaces/{}/styles.json\".format(\n                self.service_url, workspace\n            )\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            return r.json()\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def upload_style(\n        self,\n        path: str,\n        name: Optional[str] = None,\n        workspace: Optional[str] = None,\n        sld_version: str = \"1.0.0\",\n    ):\n        \"\"\"\n        Uploads a style file to geoserver.\n\n        Parameters\n        ----------\n        path : str\n            Path to the style file or XML string.\n        name : str, optional\n            The name of the style. If None, the name is parsed from the file name.\n        workspace : str, optional\n            The workspace to upload the style to.\n        sld_version : str, optional\n            The version of the SLD. Default is \"1.0.0\".\n\n        Returns\n        -------\n        int\n            The status code of the request.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue uploading the style.\n\n        Notes\n        -----\n        The name of the style file will be, sld_name:workspace\n        This function will create the style file in a specified workspace.\n        `path` can either be the path to the SLD file itself, or a string containing valid XML to be used for the style\n        Inputs: path to the sld_file or the contents of an SLD file itself, workspace,\n        \"\"\"\n        if name is None:\n            name = os.path.basename(path)\n            f = name.split(\".\")\n            if len(f) > 0:\n                name = f[0]\n\n        if is_valid_xml(path):\n            # path is actually just the xml itself\n            xml = path\n        elif Path(path).exists():\n            # path is pointing to an existing file\n            with open(path, \"rb\") as f:\n                xml = f.read()\n        else:\n            # path is non-existing file or not valid xml\n            raise ValueError(\n                \"`path` must be either a path to a style file, or a valid XML string.\"\n            )\n\n        headers = {\"content-type\": \"text/xml\"}\n\n        url = \"{}/rest/workspaces/{}/styles\".format(self.service_url, workspace)\n\n        sld_content_type = \"application/vnd.ogc.sld+xml\"\n        if sld_version == \"1.1.0\" or sld_version == \"1.1\":\n            sld_content_type = \"application/vnd.ogc.se+xml\"\n\n        header_sld = {\"content-type\": sld_content_type}\n\n        if workspace is None:\n            # workspace = \"default\"\n            url = \"{}/rest/styles\".format(self.service_url)\n\n        style_xml = \"<style><name>{}</name><filename>{}</filename></style>\".format(\n            name, name + \".sld\"\n        )\n\n        r = self._requests(method=\"post\", url=url, data=style_xml, headers=headers)\n        if r.status_code == 201:\n            r_sld = self._requests(\n                method=\"put\", url=url + \"/\" + name, data=xml, headers=header_sld\n            )\n\n            if r_sld.status_code == 200:\n                return r_sld.status_code\n            else:\n                raise GeoserverException(r_sld.status_code, r_sld.content)\n\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_coveragestyle(\n        self,\n        raster_path: str,\n        style_name: Optional[str] = None,\n        workspace: str = None,\n        color_ramp: str = \"RdYlGn_r\",\n        cmap_type: str = \"ramp\",\n        number_of_classes: int = 5,\n        opacity: float = 1,\n    ):\n        \"\"\"\n        Dynamically create style for raster.\n\n        Parameters\n        ----------\n        raster_path : str\n            Path to the raster file.\n        style_name : str, optional\n            The name of the style. If None, the name is parsed from the raster file name.\n        workspace : str\n            The workspace to create the style in.\n        color_ramp : str\n            The color ramp to use.\n        cmap_type : str\n            The type of color map.\n        number_of_classes : int\n            The number of classes.\n        opacity : float\n            The opacity of the style.\n\n        Returns\n        -------\n        int\n            The status code of the request.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating the style.\n\n        Notes\n        -----\n        The name of the style file will be, rasterName:workspace\n        This function will dynamically create the style file for raster.\n        Inputs: name of file, workspace, cmap_type (two options: values, range), ncolors: determines the number of class, min for minimum value of the raster, max for the max value of raster\n        \"\"\"\n\n        from .Calculation_gdal import raster_value\n        from .Style import coverage_style_xml\n\n        raster = raster_value(raster_path)\n        min_value = raster[\"min\"]\n        max_value = raster[\"max\"]\n        if style_name is None:\n            style_name = raster[\"file_name\"]\n        coverage_style_xml(\n            color_ramp,\n            style_name,\n            cmap_type,\n            min_value,\n            max_value,\n            number_of_classes,\n            opacity,\n        )\n        style_xml = \"<style><name>{}</name><filename>{}</filename></style>\".format(\n            style_name, style_name + \".sld\"\n        )\n\n        if style_name is None:\n            style_name = os.path.basename(raster_path)\n            f = style_name.split(\".\")\n            if len(f) > 0:\n                style_name = f[0]\n\n        headers = {\"content-type\": \"text/xml\"}\n        url = \"{}/rest/workspaces/{}/styles\".format(self.service_url, workspace)\n        sld_content_type = \"application/vnd.ogc.sld+xml\"\n        header_sld = {\"content-type\": sld_content_type}\n\n        if workspace is None:\n            url = \"{}/rest/styles\".format(self.service_url)\n\n        r = self._requests(\n            \"post\",\n            url,\n            data=style_xml,\n            headers=headers,\n        )\n        if r.status_code == 201:\n            with open(\"style.sld\", \"rb\") as f:\n                r_sld = self._requests(\n                    method=\"put\",\n                    url=url + \"/\" + style_name,\n                    data=f.read(),\n                    headers=header_sld,\n                )\n\n            os.remove(\"style.sld\")\n            if r_sld.status_code == 200:\n                return r_sld.status_code\n            else:\n                raise GeoserverException(r_sld.status_code, r_sld.content)\n\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_catagorized_featurestyle(\n        self,\n        style_name: str,\n        column_name: str,\n        column_distinct_values,\n        workspace: str = None,\n        color_ramp: str = \"tab20\",\n        geom_type: str = \"polygon\",\n    ):\n        \"\"\"\n        Dynamically create categorized style for postgis geometry,\n\n        Parameters\n        ----------\n        style_name : str\n            The name of the style.\n        column_name : str\n            The column name to base the style on.\n        column_distinct_values\n            The distinct values in the column.\n        workspace : str\n            The workspace to create the style in.\n        color_ramp : str\n            The color ramp to use.\n        geom_type : str\n            The geometry type (point, line, polygon).\n\n        Returns\n        -------\n        int\n            The status code of the request.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating the style.\n\n        Notes\n        -----\n\n        The data type must be point, line or polygon\n        Inputs: column_name (based on which column style should be generated), workspace,\n        color_or_ramp (color should be provided in hex code or the color ramp name, geom_type(point, line, polygon), outline_color(hex_color))\n        \"\"\"\n\n        from .Style import catagorize_xml\n\n        catagorize_xml(column_name, column_distinct_values, color_ramp, geom_type)\n\n        style_xml = \"<style><name>{}</name><filename>{}</filename></style>\".format(\n            style_name, style_name + \".sld\"\n        )\n\n        headers = {\"content-type\": \"text/xml\"}\n        url = \"{}/rest/workspaces/{}/styles\".format(self.service_url, workspace)\n        sld_content_type = \"application/vnd.ogc.sld+xml\"\n        header_sld = {\"content-type\": sld_content_type}\n\n        if workspace is None:\n            url = \"{}/rest/styles\".format(self.service_url)\n\n        r = self._requests(\n            \"post\",\n            url,\n            data=style_xml,\n            headers=headers,\n        )\n        if r.status_code == 201:\n            with open(\"style.sld\", \"rb\") as f:\n                r_sld = self._requests(\n                    \"put\",\n                    url + \"/\" + style_name,\n                    data=f.read(),\n                    headers=header_sld,\n                )\n            os.remove(\"style.sld\")\n            if r_sld.status_code == 200:\n                return r_sld.status_code\n            else:\n                raise GeoserverException(r_sld.status_code, r_sld.content)\n\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_outline_featurestyle(\n        self,\n        style_name: str,\n        color: str = \"#3579b1\",\n        width: str = \"2\",\n        geom_type: str = \"polygon\",\n        workspace: Optional[str] = None,\n    ):\n        \"\"\"\n        Dynamically creates the outline style for postgis geometry\n\n        Parameters\n        ----------\n        style_name : str\n            The name of the style.\n        color : str\n            The color of the outline.\n        geom_type : str\n            The geometry type (point, line, polygon).\n        workspace : str, optional\n            The workspace to create the style in.\n\n        Returns\n        -------\n        int\n            The status code of the request.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating the style.\n\n        Notes\n        -----\n        The geometry type must be point, line or polygon\n        Inputs: style_name (name of the style file in geoserver), workspace, color (style color)\n        \"\"\"\n\n        from .Style import outline_only_xml\n\n        outline_only_xml(color, width, geom_type)\n\n        style_xml = \"<style><name>{}</name><filename>{}</filename></style>\".format(\n            style_name, style_name + \".sld\"\n        )\n\n        headers = {\"content-type\": \"text/xml\"}\n        url = \"{}/rest/workspaces/{}/styles\".format(self.service_url, workspace)\n        sld_content_type = \"application/vnd.ogc.sld+xml\"\n        header_sld = {\"content-type\": sld_content_type}\n\n        if workspace is None:\n            url = \"{}/rest/styles\".format(self.service_url)\n\n        r = self._requests(\n            \"post\",\n            url,\n            data=style_xml,\n            headers=headers,\n        )\n        if r.status_code == 201:\n            with open(\"style.sld\", \"rb\") as f:\n                r_sld = self._requests(\n                    \"put\",\n                    url + \"/\" + style_name,\n                    data=f.read(),\n                    headers=header_sld,\n                )\n            os.remove(\"style.sld\")\n            if r_sld.status_code == 200:\n                return r_sld.status_code\n            else:\n                raise GeoserverException(r_sld.status_code, r_sld.content)\n\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_classified_featurestyle(\n        self,\n        style_name: str,\n        column_name: str,\n        column_distinct_values,\n        workspace: Optional[str] = None,\n        color_ramp: str = \"tab20\",\n        geom_type: str = \"polygon\",\n        # outline_color: str = \"#3579b1\",\n    ):\n        \"\"\"\n        Dynamically creates the classified style for postgis geometries.\n\n        Parameters\n        ----------\n        style_name : str\n            The name of the style.\n        column_name : str\n            The column name to base the style on.\n        column_distinct_values\n            The distinct values in the column.\n        workspace : str, optional\n            The workspace to create the style in.\n        color_ramp : str\n            The color ramp to use.\n        geom_type : str\n            The geometry type (point, line, polygon).\n\n        Returns\n        -------\n        int\n            The status code of the request.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating the style.\n\n        Notes\n        -----\n        The data type must be point, line or polygon\n        Inputs: column_name (based on which column style should be generated), workspace,\n        color_or_ramp (color should be provided in hex code or the color ramp name, geom_type(point, line, polygon), outline_color(hex_color))\n        \"\"\"\n\n        from .Style import classified_xml\n\n        classified_xml(\n            style_name,\n            column_name,\n            column_distinct_values,\n            color_ramp,\n            geom_type,\n        )\n\n        style_xml = \"<style><name>{}</name><filename>{}</filename></style>\".format(\n            column_name, column_name + \".sld\"\n        )\n\n        headers = {\"content-type\": \"text/xml\"}\n        url = \"{}/rest/workspaces/{}/styles\".format(self.service_url, workspace)\n        sld_content_type = \"application/vnd.ogc.sld+xml\"\n        header_sld = {\"content-type\": sld_content_type}\n\n        if workspace is None:\n            url = \"{}/rest/styles\".format(self.service_url)\n\n        r = self._requests(\n            \"post\",\n            url,\n            data=style_xml,\n            headers=headers,\n        )\n        if r.status_code == 201:\n            with open(\"style.sld\", \"rb\") as f:\n                r_sld = self._requests(\n                    \"put\",\n                    url + \"/\" + style_name,\n                    data=f.read(),\n                    headers=header_sld,\n                )\n            os.remove(\"style.sld\")\n            if r_sld.status_code == 200:\n                return r_sld.status_code\n            else:\n                raise GeoserverException(r_sld.status_code, r_sld.content)\n\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def publish_style(\n        self,\n        layer_name: str,\n        style_name: str,\n        workspace: str,\n    ):\n        \"\"\"\n        Publish a raster file to geoserver.\n\n        Parameters\n        ----------\n        layer_name : str\n            The name of the layer.\n        style_name : str\n            The name of the style.\n        workspace : str\n            The workspace the layer is located in.\n\n        Returns\n        -------\n        int\n            The status code of the request.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue publishing the style.\n\n        Notes\n        -----\n        The coverage store will be created automatically as the same name as the raster layer name.\n        input parameters: the parameters connecting geoserver (user,password, url and workspace name),\n        the path to the file and file_type indicating it is a geotiff, arcgrid or other raster type.\n        \"\"\"\n        headers = {\"content-type\": \"text/xml\"}\n        url = \"{}/rest/layers/{}:{}\".format(self.service_url, workspace, layer_name)\n        style_xml = (\n            \"<layer><defaultStyle><name>{}</name></defaultStyle></layer>\".format(\n                style_name\n            )\n        )\n\n        r = self._requests(\n            \"put\",\n            url,\n            data=style_xml,\n            headers=headers,\n        )\n        if r.status_code == 200:\n            return r.status_code\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def delete_style(self, style_name: str, workspace: Optional[str] = None):\n        \"\"\"\n        Delete a style from the geoserver.\n\n        Parameters\n        ----------\n        style_name : str\n            The name of the style.\n        workspace : str, optional\n            The workspace the style is located in.\n\n        Returns\n        -------\n        str\n            A success message indicating that the style was deleted.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue deleting the style.\n        \"\"\"\n        payload = {\"recurse\": \"true\"}\n        url = \"{}/rest/workspaces/{}/styles/{}\".format(\n            self.service_url, workspace, style_name\n        )\n        if workspace is None:\n            url = \"{}/rest/styles/{}\".format(self.service_url, style_name)\n\n        r = self._requests(\"delete\", url, params=payload)\n\n        if r.status_code == 200:\n            return \"Status code: {}, delete style\".format(r.status_code)\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    # _______________________________________________________________________________________________\n    #\n    #      FEATURES AND DATASTORES\n    # _______________________________________________________________________________________________\n    #\n\n    def create_featurestore(\n        self,\n        store_name: str,\n        workspace: Optional[str] = None,\n        db: str = \"postgres\",\n        host: str = \"localhost\",\n        port: int = 5432,\n        schema: str = \"public\",\n        pg_user: str = \"postgres\",\n        pg_password: str = \"admin\",\n        overwrite: bool = False,\n        expose_primary_keys: str = \"false\",\n        description: Optional[str] = None,\n        evictor_run_periodicity: Optional[int] = 300,\n        max_open_prepared_statements: Optional[int] = 50,\n        encode_functions: Optional[str] = \"false\",\n        primary_key_metadata_table: Optional[str] = None,\n        batch_insert_size: Optional[int] = 1,\n        preparedstatements: Optional[str] = \"false\",\n        loose_bbox: Optional[str] = \"true\",\n        estimated_extends: Optional[str] = \"true\",\n        fetch_size: Optional[int] = 1000,\n        validate_connections: Optional[str] = \"true\",\n        support_on_the_fly_geometry_simplification: Optional[str] = \"true\",\n        connection_timeout: Optional[int] = 20,\n        create_database: Optional[str] = \"false\",\n        min_connections: Optional[int] = 1,\n        max_connections: Optional[int] = 10,\n        evictor_tests_per_run: Optional[int] = 3,\n        test_while_idle: Optional[str] = \"true\",\n        max_connection_idle_time: Optional[int] = 300,\n    ):\n        \"\"\"\n        Create PostGIS store for connecting postgres with geoserver.\n\n        Parameters\n        ----------\n        store_name : str\n            The name of the feature store.\n        workspace : str, optional\n            The workspace to create the feature store in.\n        db : str\n            The database type. Default is \"postgres\".\n        host : str\n            The database host. Default is \"localhost\".\n        port : int\n            The database port. Default is 5432.\n        schema : str\n            The database schema. Default is \"public\".\n        pg_user : str\n            The database user. Default is \"postgres\".\n        pg_password : str\n            The database password. Default is \"admin\".\n        overwrite : bool\n            Whether to overwrite the existing feature store.\n        expose_primary_keys : str\n            Whether to expose primary keys. Default is \"false\".\n        description : str, optional\n            The description of the feature store.\n        evictor_run_periodicity : int, optional\n            The periodicity of the evictor run.\n        max_open_prepared_statements : int, optional\n            The maximum number of open prepared statements.\n        encode_functions : str, optional\n            Whether to encode functions. Default is \"false\".\n        primary_key_metadata_table : str, optional\n            The primary key metadata table.\n        batch_insert_size : int, optional\n            The batch insert size. Default is 1.\n        preparedstatements : str, optional\n            Whether to use prepared statements. Default is \"false\".\n        loose_bbox : str, optional\n            Whether to use loose bounding boxes. Default is \"true\".\n        estimated_extends : str, optional\n            Whether to use estimated extends. Default is \"true\".\n        fetch_size : int, optional\n            The fetch size. Default is 1000.\n        validate_connections : str, optional\n            Whether to validate connections. Default is \"true\".\n        support_on_the_fly_geometry_simplification : str, optional\n            Whether to support on-the-fly geometry simplification. Default is \"true\".\n        connection_timeout : int, optional\n            The connection timeout. Default is 20.\n        create_database : str, optional\n            Whether to create the database. Default is \"false\".\n        min_connections : int, optional\n            The minimum number of connections. Default is 1.\n        max_connections : int, optional\n            The maximum number of connections. Default is 10.\n        evictor_tests_per_run : int, optional\n            The number of evictor tests per run.\n        test_while_idle : str, optional\n            Whether to test while idle. Default is \"true\".\n        max_connection_idle_time : int, optional\n            The maximum connection idle time. Default is 300.\n\n        Returns\n        -------\n        str\n            A success message indicating that the feature store was created/updated.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating/updating the feature store.\n\n        Notes\n        -----\n        After creating feature store, you need to publish it. See the layer publish guidline here: https://geoserver-rest.readthedocs.io/en/latest/how_to_use.html#creating-and-publishing-featurestores-and-featurestore-layers\n        \"\"\"\n        url = \"{}/rest/workspaces/{}/datastores\".format(self.service_url, workspace)\n\n        headers = {\"content-type\": \"text/xml\"}\n\n        database_connection = \"\"\"\n                <dataStore>\n                <name>{}</name>\n                <description>{}</description>\n                <connectionParameters>\n                <entry key=\"Expose primary keys\">{}</entry>\n                <entry key=\"host\">{}</entry>\n                <entry key=\"port\">{}</entry>\n                <entry key=\"user\">{}</entry>\n                <entry key=\"passwd\">{}</entry>\n                <entry key=\"dbtype\">postgis</entry>\n                <entry key=\"schema\">{}</entry>\n                <entry key=\"database\">{}</entry>\n                <entry key=\"Evictor run periodicity\">{}</entry>\n                <entry key=\"Max open prepared statements\">{}</entry>\n                <entry key=\"encode functions\">{}</entry>\n                <entry key=\"Primary key metadata table\">{}</entry>\n                <entry key=\"Batch insert size\">{}</entry>\n                <entry key=\"preparedStatements\">{}</entry>\n                <entry key=\"Estimated extends\">{}</entry>\n                <entry key=\"fetch size\">{}</entry>\n                <entry key=\"validate connections\">{}</entry>\n                <entry key=\"Support on the fly geometry simplification\">{}</entry>\n                <entry key=\"Connection timeout\">{}</entry>\n                <entry key=\"create database\">{}</entry>\n                <entry key=\"min connections\">{}</entry>\n                <entry key=\"max connections\">{}</entry>\n                <entry key=\"Evictor tests per run\">{}</entry>\n                <entry key=\"Test while idle\">{}</entry>\n                <entry key=\"Max connection idle time\">{}</entry>\n                <entry key=\"Loose bbox\">{}</entry>\n                </connectionParameters>\n                </dataStore>\n                \"\"\".format(\n            store_name,\n            description,\n            expose_primary_keys,\n            host,\n            port,\n            pg_user,\n            pg_password,\n            schema,\n            db,\n            evictor_run_periodicity,\n            max_open_prepared_statements,\n            encode_functions,\n            primary_key_metadata_table,\n            batch_insert_size,\n            preparedstatements,\n            estimated_extends,\n            fetch_size,\n            validate_connections,\n            support_on_the_fly_geometry_simplification,\n            connection_timeout,\n            create_database,\n            min_connections,\n            max_connections,\n            evictor_tests_per_run,\n            test_while_idle,\n            max_connection_idle_time,\n            loose_bbox,\n        )\n\n        if overwrite:\n            url = \"{}/rest/workspaces/{}/datastores/{}\".format(\n                self.service_url, workspace, store_name\n            )\n\n            r = self._requests(\n                \"put\",\n                url,\n                data=database_connection,\n                headers=headers,\n            )\n        else:\n            r = self._requests(\n                \"post\",\n                url,\n                data=database_connection,\n                headers=headers,\n            )\n\n        if r.status_code in [200, 201]:\n            return \"Featurestore created/updated successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_datastore(\n        self,\n        name: str,\n        path: str,\n        workspace: Optional[str] = None,\n        overwrite: bool = False,\n        force_absolute_path: bool = True,\n    ):\n        \"\"\"\n        Create a datastore within the GeoServer.\n\n        Parameters\n        ----------\n        name : str\n            Name of datastore to be created. After creating the datastore, you need to publish it by using publish_featurestore function.\n        path : str\n            Path to shapefile (.shp) file, GeoPackage (.gpkg) file, WFS url\n            (e.g. http://localhost:8080/geoserver/wfs?request=GetCapabilities) or directory containing shapefiles.\n        workspace : str, optional\n            The workspace to create the datastore in. Default is \"default\".\n        overwrite : bool\n            Whether to overwrite the existing datastore.\n        force_absolute_path : bool, optional\n            Whether to force absolute paths (legacy behavior). Default is True.\n            Set to False to convert absolute paths to relative paths for portability.\n\n        Returns\n        -------\n        str\n            A success message indicating that the datastore was created/updated.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating/updating the datastore.\n\n        Notes\n        -----\n        If you have PostGIS datastore, please use create_featurestore function\n        \"\"\"\n        if workspace is None:\n            workspace = \"default\"\n\n        if path is None:\n            raise Exception(\"You must provide a full path to the data\")\n\n        # Handle HTTP URLs (WFS endpoints)\n        if path.startswith(\"http://\") or path.startswith(\"https://\"):\n            data_url = \"<GET_CAPABILITIES_URL>{}</GET_CAPABILITIES_URL>\".format(path)\n        else:\n            # Handle file paths with inline path conversion\n            if not force_absolute_path and os.path.isabs(path):\n                # Convert absolute path to relative path inline\n                filename = os.path.basename(path)\n                relative_path = f\"data/{workspace}/{filename}\"\n                data_url = \"<url>file:{}</url>\".format(relative_path)\n            else:\n                # Use path as-is (could be relative or absolute)\n                data_url = \"<url>file:{}</url>\".format(path)\n\n        data = \"<dataStore><name>{}</name><connectionParameters>{}</connectionParameters></dataStore>\".format(\n            name, data_url\n        )\n        headers = {\"content-type\": \"text/xml\"}\n\n        if overwrite:\n            url = \"{}/rest/workspaces/{}/datastores/{}\".format(\n                self.service_url, workspace, name\n            )\n            r = self._requests(\"put\", url, data=data, headers=headers)\n\n        else:\n            url = \"{}/rest/workspaces/{}/datastores\".format(self.service_url, workspace)\n            r = self._requests(method=\"post\", url=url, data=data, headers=headers)\n\n        if r.status_code in [200, 201]:\n            return \"Data store created/updated successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_shp_datastore(\n        self,\n        path: str,\n        store_name: Optional[str] = None,\n        workspace: Optional[str] = None,\n        file_extension: str = \"shp\",\n    ):\n        \"\"\"\n        Create datastore for a shapefile.\n\n        Parameters\n        ----------\n        path : str\n            Path to the zipped shapefile (.shp).\n        store_name : str, optional\n            Name of store to be created. If None, parses from the filename stem.\n        workspace: str, optional\n            Name of workspace to be used. Default: \"default\".\n        file_extension : str\n            The file extension of the shapefile. Default is \"shp\".\n\n        Returns\n        -------\n        str\n            A success message indicating that the shapefile datastore was created.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating the shapefile datastore.\n\n        Notes\n        -----\n        The layer name will be assigned according to the shp name\n        \"\"\"\n        if path is None:\n            raise Exception(\"You must provide a full path to shapefile\")\n\n        if workspace is None:\n            workspace = \"default\"\n\n        if store_name is None:\n            store_name = os.path.basename(path)\n            f = store_name.split(\".\")\n            if len(f) > 0:\n                store_name = f[0]\n\n        headers = {\n            \"Content-type\": \"application/zip\",\n            \"Accept\": \"application/xml\",\n        }\n\n        if isinstance(path, dict):\n            path = prepare_zip_file(store_name, path)\n\n        url = \"{0}/rest/workspaces/{1}/datastores/{2}/file.{3}?filename={2}&update=overwrite\".format(\n            self.service_url, workspace, store_name, file_extension\n        )\n\n        with open(path, \"rb\") as f:\n            r = self._requests(\"put\", url, data=f.read(), headers=headers)\n        if r.status_code in [200, 201, 202]:\n            return \"The shapefile datastore created successfully!\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_gpkg_datastore(\n        self,\n        path: str,\n        store_name: Optional[str] = None,\n        workspace: Optional[str] = None,\n        file_extension: str = \"gpkg\",\n    ):\n        \"\"\"\n        Create datastore for a geopackage.\n\n        Parameters\n        ----------\n        path : str\n            Path to the geopackage file.\n        store_name : str, optional\n            Name of store to be created. If None, parses from the filename.\n        workspace: str, optional\n            Name of workspace to be used. Default: \"default\".\n        file_extension : str\n            The file extension of the geopackage. Default is \"gpkg\".\n\n        Returns\n        -------\n        str\n            A success message indicating that the geopackage datastore was created.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating the geopackage datastore.\n\n        Notes\n        -----\n        The layer name will be assigned according to the layer name in the geopackage.\n        If the layer already exist it will be updated.\n        \"\"\"\n        if path is None:\n            raise Exception(\"You must provide a full path to shapefile\")\n\n        if workspace is None:\n            workspace = \"default\"\n\n        if store_name is None:\n            store_name = os.path.basename(path)\n            f = store_name.split(\".\")\n            if len(f) > 0:\n                store_name = f[0]\n\n        headers = {\n            \"Content-type\": \"application/x-sqlite3\",\n            \"Accept\": \"application/json\",\n        }\n\n        url = \"{0}/rest/workspaces/{1}/datastores/{2}/file.{3}?filename={2}\".format(\n            self.service_url, workspace, store_name, file_extension\n        )\n\n        with open(path, \"rb\") as f:\n            r = self._requests(\"put\", url, data=f.read(), headers=headers)\n\n        if r.status_code in [200, 201, 202]:\n            return \"The geopackage datastore created successfully!\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def publish_featurestore(\n        self,\n        store_name: str,\n        pg_table: str,\n        workspace: Optional[str] = None,\n        title: Optional[str] = None,\n        advertised: Optional[bool] = True,\n        abstract: Optional[str] = None,\n        keywords: Optional[List[str]] = None,\n        cqlfilter: Optional[str] = None,\n    ) -> int:\n        \"\"\"\n        Publish a featurestore to geoserver.\n\n        Parameters\n        ----------\n        store_name : str\n            The name of the featurestore.\n        pg_table : str\n            The name of the PostgreSQL table.\n        workspace : str, optional\n            The workspace to publish the featurestore in. Default is \"default\".\n        title : str, optional\n            The title of the featurestore. If None, the table name is used.\n        advertised : bool, optional\n            Whether to advertise the featurestore. Default is True.\n        abstract : str, optional\n            The abstract of the featurestore.\n        keywords : list of str, optional\n            List of keywords associated with the featurestore.\n        cqlfilter : str, optional\n            The CQL filter for the featurestore.\n\n        Returns\n        -------\n        int\n            The status code of the request.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue publishing the featurestore.\n\n        Notes\n        -----\n        Only user for postgis vector data\n        input parameters: specify the name of the table in the postgis database to be published, specify the store,workspace name, and  the Geoserver user name, password and URL\n        \"\"\"\n        if workspace is None:\n            workspace = \"default\"\n        if title is None:\n            title = pg_table\n\n        url = \"{}/rest/workspaces/{}/datastores/{}/featuretypes/\".format(\n            self.service_url, workspace, store_name\n        )\n\n        abstract_xml = f\"<abstract>{abstract}</abstract>\" if abstract else \"\"\n        keywords_xml = \"\"\n        if keywords:\n            keywords_xml = \"<keywords>\"\n            for keyword in keywords:\n                keywords_xml += f\"<string>{keyword}</string>\"\n            keywords_xml += \"</keywords>\"\n\n        cqlfilter_xml = f\"<cqlFilter>{cqlfilter}</cqlFilter>\" if cqlfilter else \"\"\n        layer_xml = f\"\"\"<featureType>\n                    <name>{pg_table}</name>\n                    <title>{title}</title>\n                    <advertised>{advertised}</advertised>\n                    {abstract_xml}\n                    {keywords_xml}\n                    {cqlfilter_xml}\n                </featureType>\"\"\"\n        headers = {\"content-type\": \"text/xml\"}\n\n        r = self._requests(\"post\", url, data=layer_xml, headers=headers)\n\n        if r.status_code == 201:\n            return r.status_code\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def edit_featuretype(\n        self,\n        store_name: str,\n        workspace: Optional[str],\n        pg_table: str,\n        name: str,\n        title: str,\n        abstract: Optional[str] = None,\n        keywords: Optional[List[str]] = None,\n        recalculate: Optional[str] = None,\n    ) -> int:\n        \"\"\"\n        Edit a featuretype in the geoserver.\n\n        Parameters\n        ----------\n        recalculate : str, optional\n            Recalculate param. Can be: empty string, nativebbox and nativebbox,latlonbbox.\n        store_name : str\n            The name of the feature store.\n        workspace : str, optional\n            The workspace of the feature store.\n        pg_table : str\n            The name of the PostgreSQL table.\n        name : str\n            The name of the feature type.\n        title : str\n            The title of the feature type.\n        abstract : str, optional\n            The abstract of the feature type.\n        keywords : list of str, optional\n            List of keywords associated with the feature type.\n\n        Returns\n        -------\n        int\n            The status code of the request.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue editing the feature type.\n        \"\"\"\n        if workspace is None:\n            workspace = \"default\"\n\n        recalculate_param = f\"?recalculate={recalculate}\" if recalculate else \"\"\n\n        url = \"{}/rest/workspaces/{}/datastores/{}/featuretypes/{}.xml{}\".format(\n            self.service_url, workspace, store_name, pg_table, recalculate_param\n        )\n\n        # Create XML for abstract and keywords\n        abstract_xml = f\"<abstract>{abstract}</abstract>\" if abstract else \"\"\n        keywords_xml = \"\"\n        if keywords:\n            keywords_xml = \"<keywords>\"\n            for keyword in keywords:\n                keywords_xml += f\"<string>{keyword}</string>\"\n            keywords_xml += \"</keywords>\"\n\n        layer_xml = f\"\"\"<featureType>\n                    <name>{name}</name>\n                    <title>{title}</title>\n                    {abstract_xml}{keywords_xml}\n                    </featureType>\"\"\"\n        headers = {\"content-type\": \"text/xml\"}\n\n        r = self._requests(\"put\", url, data=layer_xml, headers=headers)\n\n        if r.status_code == 200:\n            return r.status_code\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def publish_featurestore_sqlview(\n        self,\n        name: str,\n        store_name: str,\n        sql: str,\n        parameters: Optional[Iterable[Dict]] = None,\n        key_column: Optional[str] = None,\n        geom_name: str = \"geom\",\n        geom_type: str = \"Geometry\",\n        srid: Optional[int] = 4326,\n        workspace: Optional[str] = None,\n    ) -> int:\n        \"\"\"\n        Publishes an SQL query as a layer, optionally with parameters.\n\n        Parameters\n        ----------\n        name : str\n            The name of the SQL view.\n        store_name : str\n            The name of the feature store.\n        sql : str\n            The SQL query.\n        parameters : iterable of dict, optional\n            List of parameters for the SQL query.\n        key_column : str, optional\n            The key column.\n        geom_name : str, optional\n            The name of the geometry column.\n        geom_type : str, optional\n            The type of the geometry column.\n        srid : int, optional\n            The spatial reference ID. Default is 4326.\n        workspace : str, optional\n            The workspace to publish the SQL view in. Default is \"default\".\n\n        Returns\n        -------\n        int\n            The status code of the request.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue publishing the SQL view.\n\n        Notes\n        -----\n        With regards to SQL view parameters, it is advised to read the relevant section from the geoserver docs:\n        https://docs.geoserver.org/main/en/user/data/database/sqlview.html#parameterizing-sql-views\n\n        An integer-based parameter must have a default value\n\n        You should be VERY careful with the `regexp_validator`, as it can open you to SQL injection attacks. If you do\n        not supply one for a parameter, it will use the geoserver default `^[\\w\\d\\s]+$`.\n\n        The `parameters` iterable must contain dictionaries with this structure:\n\n        ```json\n        {\n          \"name\": \"<name of parameter (required)>\"\n          \"regexpValidator\": \"<string containing regex validator> (optional)\"\n          \"defaultValue\" : \"<default value of parameter if not specified (required only for non-string parameters)>\"\n        }\n        ```\n\n        \"\"\"\n        if workspace is None:\n            workspace = \"default\"\n\n        # issue #87\n        if key_column is not None:\n            key_column_xml = \"\"\"<keyColumn>{}</keyColumn>\"\"\".format(key_column)\n\n        else:\n            key_column_xml = \"\"\"\"\"\"\n\n        parameters_xml = \"\"\n        if parameters is not None:\n            for parameter in parameters:\n\n                # non-string parameters MUST have a default value supplied\n                if (\n                    not is_surrounded_by_quotes(sql, parameter[\"name\"])\n                    and not \"defaultValue\" in parameter\n                ):\n                    raise ValueError(\n                        f\"Parameter `{parameter['name']}` appears to be a non-string in the supplied query\"\n                        \", but does not have a default value specified. You must supply a default value \"\n                        \"for non-string parameters using the `defaultValue` key.\"\n                    )\n\n                param_name = parameter.get(\"name\", \"\")\n                default_value = parameter.get(\"defaultValue\", \"\")\n                regexp_validator = parameter.get(\"regexpValidator\", r\"^[\\w\\d\\s]+$\")\n                parameters_xml += f\"\"\"\n                    <parameter>\n                        <name>{param_name}</name>\n                        <defaultValue>{default_value}</defaultValue>\n                        <regexpValidator>{regexp_validator}</regexpValidator>\n                    </parameter>\\n\n                \"\"\".strip()\n\n        layer_xml = \"\"\"<featureType>\n        <name>{0}</name>\n        <enabled>true</enabled>\n        <namespace>\n            <name>{4}</name>\n        </namespace>\n        <title>{0}</title>\n        <srs>EPSG:{5}</srs>\n        <metadata>\n            <entry key=\"JDBC_VIRTUAL_TABLE\">\n                <virtualTable>\n                    <name>{0}</name>\n                    <sql>{1}</sql>\n                    <escapeSql>true</escapeSql>\n                    <geometry>\n                        <name>{2}</name>\n                        <type>{3}</type>\n                        <srid>{5}</srid>\n                    </geometry>{6}\n                    {7}\n                </virtualTable>\n            </entry>\n        </metadata>\n        </featureType>\"\"\".format(\n            name,\n            sql,\n            geom_name,\n            geom_type,\n            workspace,\n            srid,\n            key_column_xml,\n            parameters_xml,\n        )\n\n        # rest API url\n        url = \"{}/rest/workspaces/{}/datastores/{}/featuretypes\".format(\n            self.service_url, workspace, store_name\n        )\n\n        # headers\n        headers = {\"content-type\": \"text/xml\"}\n\n        # request\n        r = self._requests(\"post\", url, data=layer_xml, headers=headers)\n\n        if r.status_code == 201:\n            return r.status_code\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_featuretypes(\n        self, workspace: str = None, store_name: str = None\n    ) -> List[str]:\n        \"\"\"\n        Get feature types from the geoserver.\n\n        Parameters\n        ----------\n        workspace : str\n            The workspace to get the feature types from.\n        store_name : str\n            The name of the feature store.\n\n        Returns\n        -------\n        list of str\n            A list of feature types.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue getting the feature types.\n        \"\"\"\n        url = \"{}/rest/workspaces/{}/datastores/{}/featuretypes.json\".format(\n            self.service_url, workspace, store_name\n        )\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            r_dict = r.json()\n            features = [i[\"name\"] for i in r_dict[\"featureTypes\"][\"featureType\"]]\n            return features\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_feature_attribute(\n        self, feature_type_name: str, workspace: str, store_name: str\n    ) -> List[str]:\n        \"\"\"\n        Get feature attributes from the geoserver.\n\n        Parameters\n        ----------\n        feature_type_name : str\n            The name of the feature type.\n        workspace : str\n            The workspace of the feature store.\n        store_name : str\n            The name of the feature store.\n\n        Returns\n        -------\n        list of str\n            A list of feature attributes.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue getting the feature attributes.\n        \"\"\"\n        url = \"{}/rest/workspaces/{}/datastores/{}/featuretypes/{}.json\".format(\n            self.service_url, workspace, store_name, feature_type_name\n        )\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            r_dict = r.json()\n            attribute = [\n                i[\"name\"] for i in r_dict[\"featureType\"][\"attributes\"][\"attribute\"]\n            ]\n            return attribute\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_featurestore(self, store_name: str, workspace: str) -> dict:\n        \"\"\"\n        Get a featurestore from the geoserver.\n\n        Parameters\n        ----------\n        store_name : str\n            The name of the feature store.\n        workspace : str\n            The workspace of the feature store.\n\n        Returns\n        -------\n        dict\n            A dictionary representation of the feature store.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue getting the feature store.\n        \"\"\"\n        url = \"{}/rest/workspaces/{}/datastores/{}\".format(\n            self.service_url, workspace, store_name\n        )\n        r = self._requests(\"get\", url)\n        if r.status_code == 200:\n            r_dict = r.json()\n            return r_dict[\"dataStore\"]\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def delete_featurestore(\n        self, featurestore_name: str, workspace: Optional[str] = None\n    ) -> str:\n        \"\"\"\n        Delete a featurestore from the geoserver.\n\n        Parameters\n        ----------\n        featurestore_name : str\n            The name of the featurestore.\n        workspace : str, optional\n            The workspace of the featurestore.\n\n        Returns\n        -------\n        str\n            A success message indicating that the featurestore was deleted.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue deleting the featurestore.\n        \"\"\"\n        payload = {\"recurse\": \"true\"}\n        url = \"{}/rest/workspaces/{}/datastores/{}\".format(\n            self.service_url, workspace, featurestore_name\n        )\n        if workspace is None:\n            url = \"{}/datastores/{}\".format(self.service_url, featurestore_name)\n        r = self._requests(\"delete\", url, params=payload)\n\n        if r.status_code == 200:\n            return \"Status code: {}, delete featurestore\".format(r.status_code)\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def delete_coveragestore(\n        self, coveragestore_name: str, workspace: Optional[str] = None\n    ) -> str:\n        \"\"\"\n        Delete a coveragestore from the geoserver.\n\n        Parameters\n        ----------\n        coveragestore_name : str\n            The name of the coveragestore.\n        workspace : str, optional\n            The workspace of the coveragestore.\n\n        Returns\n        -------\n        str\n            A success message indicating that the coveragestore was deleted.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue deleting the coveragestore.\n        \"\"\"\n        payload = {\"recurse\": \"true\"}\n        url = \"{}/rest/workspaces/{}/coveragestores/{}\".format(\n            self.service_url, workspace, coveragestore_name\n        )\n\n        if workspace is None:\n            url = \"{}/rest/coveragestores/{}\".format(\n                self.service_url, coveragestore_name\n            )\n\n        r = self._requests(\"delete\", url, params=payload)\n\n        if r.status_code == 200:\n            return \"Coverage store deleted successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    # _______________________________________________________________________________________________\n    #\n    #      USERS AND USERGROUPS\n    # _______________________________________________________________________________________________\n    #\n\n    def get_all_users(self, service=None) -> dict:\n        \"\"\"\n        Query all users in the provided user/group service, else default user/group service is queried.\n\n        Parameters\n        ----------\n        service: str, optional\n            The user/group service to query.\n\n        Returns\n        -------\n        dict\n            A dictionary containing all users.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue getting the users.\n        \"\"\"\n        url = \"{}/rest/security/usergroup/\".format(self.service_url)\n        if service is None:\n            url += \"users/\"\n        else:\n            url += \"service/{}/users/\".format(service)\n\n        headers = {\"accept\": \"application/xml\"}\n        r = self._requests(\"get\", url, headers=headers)\n\n        if r.status_code == 200:\n            return parse(r.content)\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_user(\n        self, username: str, password: str, enabled: bool = True, service=None\n    ) -> str:\n        \"\"\"\n        Add a new user to the provided user/group service.\n\n        Parameters\n        ----------\n        username : str\n            The username of the new user.\n        password: str\n            The password of the new user.\n        enabled: bool\n            Whether the new user is enabled.\n        service : str, optional\n            The user/group service to add the user to.\n\n        Returns\n        -------\n        str\n            A success message indicating that the user was created.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating the user.\n        \"\"\"\n        url = \"{}/rest/security/usergroup/\".format(self.service_url)\n        if service is None:\n            url += \"users/\"\n        else:\n            url += \"service/{}/users/\".format(service)\n\n        data = \"<user><userName>{}</userName><password>{}</password><enabled>{}</enabled></user>\".format(\n            username, password, str(enabled).lower()\n        )\n        headers = {\"content-type\": \"text/xml\", \"accept\": \"application/json\"}\n        r = self._requests(\"post\", url, data=data, headers=headers)\n\n        if r.status_code == 201:\n            return \"User created successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def modify_user(\n        self, username: str, new_name=None, new_password=None, enable=None, service=None\n    ) -> str:\n        \"\"\"\n        Modifies a user in the provided user/group service.\n\n        Parameters\n        ----------\n        username : str\n            The username of the user to modify.\n        new_name : str, optional\n            The new username.\n        new_password : str, optional\n            The new password.\n        enable : bool, optional\n            Whether the user is enabled.\n        service : str, optional\n            The user/group service to modify the user in.\n\n        Returns\n        -------\n        str\n            A success message indicating that the user was modified.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue modifying the user.\n        \"\"\"\n        url = \"{}/rest/security/usergroup/\".format(self.service_url)\n        if service is None:\n            url += \"user/{}\".format(username)\n        else:\n            url += \"service/{}/user/{}\".format(service, username)\n\n        modifications = dict()\n        if new_name is not None:\n            modifications[\"userName\"] = new_name\n        if new_password is not None:\n            modifications[\"password\"] = new_password\n        if enable is not None:\n            modifications[\"enabled\"] = enable\n\n        data = unparse({\"user\": modifications})\n        print(url, data)\n        headers = {\"content-type\": \"text/xml\", \"accept\": \"application/json\"}\n        r = self._requests(\"post\", url, data=data, headers=headers)\n\n        if r.status_code == 200:\n            return \"User modified successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def delete_user(self, username: str, service=None) -> str:\n        \"\"\"\n        Deletes user from the provided user/group service.\n\n        Parameters\n        ----------\n        username : str\n            The username of the user to delete.\n        service : str, optional\n            The user/group service to delete the user from.\n\n        Returns\n        -------\n        str\n            A success message indicating that the user was deleted.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue deleting the user.\n        \"\"\"\n        url = \"{}/rest/security/usergroup/\".format(self.service_url)\n        if service is None:\n            url += \"user/{}\".format(username)\n        else:\n            url += \"service/{}/user/{}\".format(service, username)\n\n        headers = {\"accept\": \"application/json\"}\n        r = self._requests(\"delete\", url, headers=headers)\n\n        if r.status_code == 200:\n            return \"User deleted successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def get_all_usergroups(self, service=None) -> dict:\n        \"\"\"\n        Queries all the groups in the given user/group service.\n\n        Parameters\n        ----------\n        service : str, optional\n            The user/group service to query.\n\n        Returns\n        -------\n        dict\n            A dictionary containing all user groups.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue getting the user groups.\n        \"\"\"\n        url = \"{}/rest/security/usergroup/\".format(self.service_url)\n        if service is None:\n            url += \"groups/\"\n        else:\n            url += \"service/{}/groups/\".format(service)\n\n        r = self._requests(\"get\", url)\n\n        if r.status_code == 200:\n            return parse(r.content)\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def create_usergroup(self, group: str, service=None) -> str:\n        \"\"\"\n        Add a new usergroup to the provided user/group service.\n\n        Parameters\n        ----------\n        group : str\n            The name of the user group.\n        service : str, optional\n            The user/group service to add the user group to.\n\n        Returns\n        -------\n        str\n            A success message indicating that the user group was created.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue creating the user group.\n        \"\"\"\n        url = \"{}/rest/security/usergroup/\".format(self.service_url)\n        if service is None:\n            url += \"group/{}\".format(group)\n        else:\n            url += \"service/{}/group/{}\".format(service, group)\n        r = self._requests(\"post\", url)\n\n        if r.status_code == 201:\n            return \"Group created successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    def delete_usergroup(self, group: str, service=None) -> str:\n        \"\"\"\n        Deletes given usergroup from provided user/group service.\n\n        Parameters\n        ----------\n        group : str\n            The name of the user group to delete.\n        service : str, optional\n            The user/group service to delete the user group from.\n\n        Returns\n        -------\n        str\n            A success message indicating that the user group was deleted.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue deleting the user group.\n        \"\"\"\n        url = \"{}/rest/security/usergroup/\".format(self.service_url)\n        if service is None:\n            url += \"group/{}\".format(group)\n        else:\n            url += \"service/{}/group/{}\".format(service, group)\n\n        r = self._requests(\"delete\", url)\n\n        if r.status_code == 200:\n            return \"Group deleted successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n\n    # _______________________________________________________________________________________________\n    #\n    #      SERVICES\n    # _______________________________________________________________________________________________\n    #\n\n    def update_service(self, service: str, **kwargs):\n        \"\"\"\n        Update selected service's options.\n\n        Parameters\n        ----------\n        service : str\n            Type of service (e.g., wms, wfs)\n        kwargs : dict\n            Options to be modified (e.g., maxRenderingTime=600)\n\n        Returns\n        -------\n        str\n            A success message indicating that the options were updated.\n\n        Raises\n        ------\n        GeoserverException\n            If there is an issue updating the service's options.\n        \"\"\"\n        url = \"{}/rest/services/{}/settings\".format(self.service_url, service)\n        headers = {\"content-type\": \"text/xml\"}\n\n        data = \"\"\n        for key, value in kwargs.items():\n            data += \"<{}><{}>{}</{}></{}>\".format(service, key, value, key, service)\n\n        r = self._requests(\"put\", url, data=data, headers=headers)\n\n        if r.status_code == 200:\n            return \"Service's option updated successfully\"\n        else:\n            raise GeoserverException(r.status_code, r.content)\n"
  },
  {
    "path": "geo/Style.py",
    "content": "# inbuilt libraries\nfrom typing import Dict, Iterable, List, Union\n\n# third-party libraries\nimport seaborn as sns\nfrom matplotlib.colors import rgb2hex\n\n\ndef coverage_style_colormapentry(\n    color_ramp: Union[List, Dict, Iterable],\n    min_value: float,\n    max_value: float,\n    number_of_classes: int = None,\n):\n    \"\"\"\n\n    Parameters\n    ----------\n    color_ramp\n    min_value\n    max_value\n    number_of_classes\n\n    Returns\n    -------\n\n    Notes\n    -----\n    This is the core function for controlling the layers styles\n    The color_ramp can be list or dict or touple or str\n    min, max will be dynamically calculated value from raster\n    number_of_classes will be available in map legend\n    \"\"\"\n    style_append = \"\"\n    n = len(color_ramp)\n\n    if isinstance(color_ramp, list):\n\n        if n != number_of_classes:\n            number_of_classes = n\n\n        interval = (max_value - min_value) / (number_of_classes - 1)\n\n        for i, color in enumerate(color_ramp):\n            value = min_value + interval * i\n            value = round(value, 1)\n\n            style_append += (\n                '<sld:ColorMapEntry color=\"{}\" label=\"{}\" quantity=\"{}\"/>'.format(\n                    color, value, value\n                )\n            )\n\n    elif isinstance(color_ramp, dict):\n\n        if n != number_of_classes:\n            number_of_classes = n\n\n        interval = (max_value - min_value) / (number_of_classes - 1)\n\n        for name, color, i in zip(color_ramp.keys(), color_ramp.values(), range(n)):\n            value = min_value + interval * i\n\n            style_append += (\n                '<sld:ColorMapEntry color=\"{}\" label=\" {}\" quantity=\"{}\"/>'.format(\n                    color, name, value\n                )\n            )\n\n    else:\n        for i, color in enumerate(color_ramp):\n            interval = (max_value - min_value) / (number_of_classes - 1)\n            value = min_value + interval * i\n\n            style_append += (\n                '<sld:ColorMapEntry color=\"{}\" label=\"{}\" quantity=\"{}\"/>'.format(\n                    color, value, value\n                )\n            )\n\n    return style_append\n\n\ndef coverage_style_xml(\n    color_ramp, style_name, cmap_type, min_value, max_value, number_of_classes, opacity\n):\n    min_max_difference = max_value - min_value\n    style_append = \"\"\n    interval = min_max_difference / (number_of_classes - 1)  # noqa\n\n    # The main style of the coverage style\n    if isinstance(color_ramp, str):\n        palette = sns.color_palette(color_ramp, int(number_of_classes))\n        color_ramp = [rgb2hex(i) for i in palette]\n\n    style_append += coverage_style_colormapentry(\n        color_ramp, min_value, max_value, number_of_classes\n    )\n\n    style = \"\"\"\n    <StyledLayerDescriptor xmlns=\"http://www.opengis.net/sld\" xmlns:gml=\"http://www.opengis.net/gml\" version=\"1.0.0\" xmlns:ogc=\"http://www.opengis.net/ogc\" xmlns:sld=\"http://www.opengis.net/sld\">\n    <UserLayer>\n        <sld:LayerFeatureConstraints>\n        <sld:FeatureTypeConstraint/>\n        </sld:LayerFeatureConstraints>\n        <sld:UserStyle>\n        <sld:Name>{2}</sld:Name>\n        <sld:FeatureTypeStyle>\n            <sld:Rule>\n            <sld:RasterSymbolizer>\n                <sld:Opacity>{3}</sld:Opacity>\n                <sld:ChannelSelection>\n                <sld:GrayChannel>\n                    <sld:SourceChannelName>1</sld:SourceChannelName>\n                </sld:GrayChannel>\n                </sld:ChannelSelection>\n                <sld:ColorMap type=\"{0}\">\n                    {1}\n                </sld:ColorMap>\n            </sld:RasterSymbolizer>\n            </sld:Rule>\n        </sld:FeatureTypeStyle>\n        </sld:UserStyle>\n    </UserLayer>\n    </StyledLayerDescriptor>\n    \"\"\".format(\n        cmap_type, style_append, style_name, opacity\n    )\n\n    with open(\"style.sld\", \"w\") as f:\n        f.write(style)\n\n\ndef outline_only_xml(color, width, geom_type=\"polygon\"):\n    if geom_type == \"point\":\n        symbolizer = \"\"\"\n            <PointSymbolizer>\n                <Graphic>\n                <Mark>\n                    <WellKnownName>circle</WellKnownName>\n                    <Fill>\n                    <CssParameter name=\"fill\">{}</CssParameter>\n                    </Fill>\n                </Mark>\n                <Size>8</Size>\n                </Graphic>\n            </PointSymbolizer>\n        \"\"\".format(\n            color\n        )\n\n    elif geom_type == \"line\":\n        symbolizer = \"\"\"\n                <LineSymbolizer>\n                    <Stroke>\n                    <CssParameter name=\"stroke\">{}</CssParameter>\n                    <CssParameter name=\"stroke-width\"{}</CssParameter>\n                    </Stroke>\n                </LineSymbolizer>\n            \"\"\".format(\n            color, width\n        )\n\n    elif geom_type == \"polygon\":\n        symbolizer = \"\"\"\n                <PolygonSymbolizer>\n                    <Stroke>\n                    <CssParameter name=\"stroke\">{}</CssParameter>\n                    <CssParameter name=\"stroke-width\">{}</CssParameter>\n                    </Stroke>\n                </PolygonSymbolizer>\n            \"\"\".format(\n            color, width\n        )\n\n    else:\n        print(\"Error: Invalid geometry type\")\n        return\n\n    style = \"\"\"\n            <StyledLayerDescriptor xmlns=\"http://www.opengis.net/sld\" xmlns:ogc=\"http://www.opengis.net/ogc\" xmlns:se=\"http://www.opengis.net/se\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xsi:schemaLocation=\"http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd\" version=\"1.1.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n            <NamedLayer>\n                <se:Name>Layer name</se:Name>\n                <UserStyle>\n                <se:Name>Layer name</se:Name>\n                <se:FeatureTypeStyle>\n                    <se:Rule>\n                    <se:Name>Single symbol</se:Name>\n                    {}\n                    </se:Rule>\n                </se:FeatureTypeStyle>\n                </UserStyle>\n            </NamedLayer>\n            </StyledLayerDescriptor>\n            \"\"\".format(\n        symbolizer\n    )\n\n    with open(\"style.sld\", \"w\") as f:\n        f.write(style)\n\n\ndef catagorize_xml(\n    column_name: str,\n    values: List[float],\n    color_ramp: str = None,\n    geom_type: str = \"polygon\",\n):\n    n = len(values)\n    palette = sns.color_palette(color_ramp, int(n))\n    palette_hex = [rgb2hex(i) for i in palette]\n    rule = \"\"\n    for value, color in zip(values, palette_hex):\n        if geom_type == \"point\":\n            rule += \"\"\"\n                <Rule>\n                <Name>{0}</Name>\n                <Title>{1}</Title>\n                <ogc:Filter>\n                    <ogc:PropertyIsEqualTo>\n                    <ogc:PropertyName>{0}</ogc:PropertyName>\n                    <ogc:Literal>{1}</ogc:Literal>\n                    </ogc:PropertyIsEqualTo>\n                </ogc:Filter>\n                <PointSymbolizer>\n                    <Graphic>\n                    <Mark>\n                        <WellKnownName>circle</WellKnownName>\n                        <Fill>\n                        <CssParameter name=\"fill\">{2}</CssParameter>\n                        </Fill>\n                    </Mark>\n                    <Size>5</Size>\n                    </Graphic>\n                </PointSymbolizer>\n                </Rule>\n            \"\"\".format(\n                column_name, value, color\n            )\n\n        elif geom_type == \"line\":\n            rule += \"\"\"\n                <Rule>\n                    <Name>{1}</Name>\n                    <ogc:Filter>\n                        <ogc:PropertyIsEqualTo>\n                        <ogc:PropertyName>{0}</ogc:PropertyName>\n                        <ogc:Literal>{1}</ogc:Literal>\n                        </ogc:PropertyIsEqualTo>\n                    </ogc:Filter>\n                    <LineSymbolizer>\n                        <Stroke>\n                        <CssParameter name=\"stroke\">{2}</CssParameter>\n                        <CssParameter name=\"stroke-width\">1</CssParameter>\n                        </Stroke>\n                    </LineSymbolizer>\n                </Rule>\n            \"\"\".format(\n                column_name, value, color\n            )\n\n        elif geom_type == \"polygon\":\n            rule += \"\"\"\n                <Rule>\n                    <Name>{0}</Name>\n                    <Title>{1}</Title>\n                    <ogc:Filter>\n                        <ogc:PropertyIsEqualTo>\n                        <ogc:PropertyName>{0}</ogc:PropertyName>\n                        <ogc:Literal>{1}</ogc:Literal>\n                        </ogc:PropertyIsEqualTo>\n                    </ogc:Filter>\n                    <PolygonSymbolizer>\n                        <Fill>\n                            <CssParameter name=\"fill\">{2}</CssParameter>\n                        </Fill>\n                        <Stroke>\n                            <CssParameter name=\"stroke\">{3}</CssParameter>\n                            <CssParameter name=\"stroke-width\">0.5</CssParameter>\n                        </Stroke>\n                    </PolygonSymbolizer>\n                </Rule>\n\n            \"\"\".format(\n                column_name, value, color, \"#000000\"\n            )\n\n        else:\n            print(\"Error: Invalid geometry type\")\n            return\n\n    style = \"\"\"\n            <StyledLayerDescriptor xmlns=\"http://www.opengis.net/sld\" xmlns:ogc=\"http://www.opengis.net/ogc\" xmlns:se=\"http://www.opengis.net/se\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xsi:schemaLocation=\"http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd\" version=\"1.1.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n                    <NamedLayer>\n                        <se:Name>Layer name</se:Name>\n                        <UserStyle>\n                        <se:Name>Layer name</se:Name>\n                        <FeatureTypeStyle>\n                            {}\n                        </FeatureTypeStyle>\n                        </UserStyle>\n                    </NamedLayer>\n                </StyledLayerDescriptor>\n        \"\"\".format(\n        rule\n    )\n\n    with open(\"style.sld\", \"w\") as f:\n        f.write(style)\n\n\ndef classified_xml(\n    style_name: str,\n    column_name: str,\n    values: List[float],\n    color_ramp: str = None,\n    geom_type: str = \"polygon\",\n):\n    max_value = max(values)\n    min_value = min(values)\n    diff = max_value - min_value\n    n = 5\n    interval = diff / 5\n    palette = sns.color_palette(color_ramp, int(n))\n    palette_hex = [rgb2hex(i) for i in palette]\n    # interval = N/4\n    # color_values = [{value: color} for value, color in zip(values, palette_hex)]\n    # print(color_values)\n    rule = \"\"\n    for i, color in enumerate(palette_hex):\n        print(i)\n\n        rule += \"\"\"\n            <se:Rule>\n                <se:Name>{1}</se:Name>\n                <se:Description>\n                    <se:Title>{4}</se:Title>\n                </se:Description>\n                <ogc:Filter xmlns:ogc=\"http://www.opengis.net/ogc\">\n                    <ogc:And>\n                    <ogc:PropertyIsGreaterThan>\n                        <ogc:PropertyName>{0}</ogc:PropertyName>\n                        <ogc:Literal>{5}</ogc:Literal>\n                    </ogc:PropertyIsGreaterThan>\n                    <ogc:PropertyIsLessThanOrEqualTo>\n                        <ogc:PropertyName>{0}</ogc:PropertyName>\n                        <ogc:Literal>{4}</ogc:Literal>\n                    </ogc:PropertyIsLessThanOrEqualTo>\n                    </ogc:And>\n                </ogc:Filter>\n                <se:PolygonSymbolizer>\n                    <se:Fill>\n                    <se:SvgParameter name=\"fill\">{2}</se:SvgParameter>\n                    </se:Fill>\n                    <se:Stroke>\n                    <se:SvgParameter name=\"stroke\">{3}</se:SvgParameter>\n                    <se:SvgParameter name=\"stroke-width\">1</se:SvgParameter>\n                    <se:SvgParameter name=\"stroke-linejoin\">bevel</se:SvgParameter>\n                    </se:Stroke>\n                </se:PolygonSymbolizer>\n            </se:Rule>\n\n        \"\"\".format(\n            column_name,\n            style_name,\n            color,\n            \"#000000\",\n            min_value + interval * i,\n            min_value + interval * (i + 1),\n        )\n\n    style = \"\"\"\n            <StyledLayerDescriptor xmlns=\"http://www.opengis.net/sld\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:ogc=\"http://www.opengis.net/ogc\" version=\"1.1.0\" xmlns:se=\"http://www.opengis.net/se\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd\">\n                <NamedLayer>\n                    <se:Name>{0}</se:Name>\n                    <UserStyle>\n                    <se:Name>{0}</se:Name>\n                        <se:FeatureTypeStyle>\n                            {1}\n                        </se:FeatureTypeStyle>\n                    </UserStyle>\n                </NamedLayer>\n            </StyledLayerDescriptor>\n        \"\"\".format(\n        style_name, rule\n    )\n\n    with open(\"style.sld\", \"w\") as f:\n        f.write(style)\n"
  },
  {
    "path": "geo/__init__.py",
    "content": "\n"
  },
  {
    "path": "geo/__version__.py",
    "content": "# This information is located in its own file so that it can be loaded\n# without importing the main package when its dependencies are not installed.\n# See: https://packaging.python.org/guides/single-sourcing-package-version\n\n__author__ = \"Tek Kshetri\"\n__email__ = \"iamtekson@gmail.com\"\n__version__ = \"2.10.0\"\n"
  },
  {
    "path": "geo/supports.py",
    "content": "import os\nimport re\nfrom tempfile import mkstemp\nfrom typing import Dict\nfrom zipfile import ZipFile\nimport xml.etree.ElementTree as ET\n\n\ndef prepare_zip_file(name: str, data: Dict) -> str:\n    \"\"\"Creates a zip file from\n\n    GeoServer's REST API uses ZIP archives as containers for file formats such\n    as Shapefile and WorldImage which include several 'boxcar' files alongside\n    the main data.  In such archives, GeoServer assumes that all of the relevant\n    files will have the same base name and appropriate extensions, and live in\n    the root of the ZIP archive.  This method produces a zip file that matches\n    these expectations, based on a basename, and a dict of extensions to paths or\n    file-like objects. The client code is responsible for deleting the zip\n    archive when it's done.\n\n    Parameters\n    ----------\n    name : name of files\n    data : dict\n\n    Returns\n    -------\n    str\n    \"\"\"\n    fd, path = mkstemp()\n    zip_file = ZipFile(path, \"w\", allowZip64=True)\n    print(fd, path, zip_file, data)\n    for ext, stream in data.items():\n        fname = \"{}.{}\".format(name, ext)\n        if isinstance(stream, str):\n            zip_file.write(stream, fname)\n        else:\n            zip_file.writestr(fname, stream.read())\n    zip_file.close()\n    os.close(fd)\n    return path\n\n\ndef is_valid_xml(xml_string: str) -> bool:\n\n    \"\"\"\n    Returns True if string is valid XML, false otherwise\n\n        Parameters\n    ----------\n    xml_string : string containing xml\n\n    Returns\n    -------\n    bool\n    \"\"\"\n\n    try:\n        # Attempt to parse the XML string\n        ET.fromstring(xml_string)\n        return True\n    except ET.ParseError:\n        return False\n\n\ndef is_surrounded_by_quotes(text, param):\n    # The regex pattern searches for '%foo%' surrounded by single quotes.\n    # It uses \\'%foo%\\' to match '%foo%' literally, including the single quotes.\n    pattern = rf\"\\'%{param}%\\'\"\n\n    # re.search() searches the string for the first location where the regex pattern produces a match.\n    # If a match is found, re.search() returns a match object. Otherwise, it returns None.\n    match = re.search(pattern, text)\n\n    # Return True if a match is found, False otherwise.\n    return bool(match)\n"
  },
  {
    "path": "requirements.txt",
    "content": "pygments\nrequests\nxmltodict"
  },
  {
    "path": "requirements_dev.txt",
    "content": "-r requirements.txt\n\npytest\nblack\nflake8\nsphinx>=1.7\nsphinx-rtd-theme>=2.0\npre-commit\nenvirons\nddt\nsqlalchemy>=2.0.29\npsycopg2>=2.9.9\ngdal>=3.4.1  # Note: in the automated test pipeline, this will be replaced by whatever is the output of `gdal-config --version`\nseaborn>=0.13.2\nddt>=1.7.1\nxmltodict>=0.13.0\n"
  },
  {
    "path": "requirements_style.txt",
    "content": "matplotlib\nseaborn\ngdal"
  },
  {
    "path": "setup.cfg",
    "content": "[flake8]\nignore =\n\tC901\n\tE203\n\tE231\n\tE266\n\tE501\n\tF401\n\tF403\n\tW503\n\tW504\nmax-line-length = 88\nmax-complexity = 12\nexclude =\n\t.git,\n\t__pycache__,\n\tdocs/source/conf.py,\n\tbuild,\n\tdist,\n\tsrc,\n\t.eggs,\n\n[isort]\nprofile = black\n\n[tool:pytest]\nnorecursedirs = src .git bin conda-recipe joss\npython_files = test_*.py\n"
  },
  {
    "path": "setup.py",
    "content": "import os\nfrom typing import Dict\n\nfrom setuptools import setup\n\nHERE = os.path.abspath(os.path.dirname(__file__))\n\nabout = dict()\n\nwith open(os.path.join(HERE, \"geo\", \"__version__.py\")) as f:\n    exec(f.read(), about)\n\nwith open(\"README.md\") as fh:\n    long_description = fh.read()\n\nsetup(\n    name=\"geoserver-rest\",\n    version=about[\"__version__\"],\n    author=about[\"__author__\"],\n    author_email=about[\"__email__\"],\n    description=\"Package for GeoServer rest API\",\n    py_modules=[\"geoserver-rest-python\"],\n    # package_dir={'':'src'},\n    license=\"MIT License\",\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/iamtekson/geoserver-rest-python\",\n    packages=[\"geo\"],\n    keywords=[\n        \"geoserver-rest-python\",\n        \"geoserver rest\",\n        \"python geoserver\",\n        \"geoserver api\",\n        \"api\",\n        \"rest geoserver\",\n        \"python\",\n        \"geoserver python\",\n        \"geoserver rest\",\n    ],\n    classifiers=[\n        \"Programming Language :: Python :: 3\",\n        \"Intended Audience :: Developers\",\n        \"Intended Audience :: Science/Research\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Operating System :: OS Independent\",\n    ],\n    install_requires=[\n        \"pygments\",\n        \"requests\",\n        \"xmltodict\",\n    ],\n    extras_require={\"dev\": [\"pytest\", \"black\", \"flake8\", \"sphinx>=1.7\", \"pre-commit\"], 'style': ['matplotlib', 'seaborn', 'gdal'], 'all': ['matplotlib', 'seaborn', 'gdal']},\n    python_requires=\">=3.6\",\n)\n"
  },
  {
    "path": "tests/.env_template",
    "content": "# Login information for geoserver\nGEOSERVER_URL=\"http://localhost:8080/geoserver\"\nGEOSERVER_USER=\"admin\"\nGEOSERVER_PASSWORD=\"geoserver\"\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/common.py",
    "content": "import os\n\nfrom geo.Geoserver import Geoserver\n\nGEO_URL = os.getenv(\"GEO_URL\", \"http://localhost:8080/geoserver\")  # relative to test machine\n\ngeo = Geoserver(GEO_URL, username=os.getenv(\"GEO_USER\", \"admin\"), password=os.getenv(\"GEO_PASS\", \"geoserver\"))\n\npostgis_params = {\n    \"host\": os.getenv(\"DB_HOST\", \"localhost\"),  # relative to the geoserver instance\n    \"port\": os.getenv(\"DB_PORT\", \"5432\"),  # relative to the geoserver instance\n    \"db\": os.getenv(\"DB_NAME\", \"geodb\"),\n    \"pg_user\": os.getenv(\"DB_USER\", \"geodb_user\"),\n    \"pg_password\": os.getenv(\"DB_PASS\", \"geodb_pass\")\n}\n\n# in case you are using docker or something, and the location of the database is different relative to your host machine\npostgis_params_local_override = {\n    \"host\": os.getenv(\"DB_HOST_LOCAL\", \"localhost\"),  # relative to the test machine\n    \"port\": os.getenv(\"DB_PORT_LOCAL\", \"5432\"),  # relative to the test machine\n}\npostgis_params_local = {**postgis_params, **postgis_params_local_override}\n"
  },
  {
    "path": "tests/docker-compose.yaml",
    "content": "version: '3.8'\n\nservices:\n\n  postgis:\n    image: postgis/postgis:latest\n    environment:\n      POSTGRES_DB: geodb\n      POSTGRES_USER: geodb_user\n      POSTGRES_PASSWORD: geodb_pass\n    ports:\n      - \"0.0.0.0:5432:5432\"\n\n  geoserver:\n    image: kartoza/geoserver:latest\n    environment:\n      - GEOSERVER_ADMIN_PASSWORD=geoserver\n      - GEOSERVER_ADMIN_USER=admin\n      - SAMPLE_DATA=true\n    ports:\n      - \"0.0.0.0:8080:8080\"\n"
  },
  {
    "path": "tests/test_geoserver.py",
    "content": "import os\nimport pathlib\n\nimport requests\nimport pytest\nimport sqlalchemy as sa\n\nfrom geo.Style import catagorize_xml, classified_xml\nfrom geo.Geoserver import GeoserverException, Geoserver\n\nfrom .common import GEO_URL, geo, postgis_params, postgis_params_local\n\nHERE = pathlib.Path(__file__).parent.resolve()\n\n\nclass TestCustomRequestParameters:\n\n    def test_custom_request_parameters(self):\n        \"\"\"\n        Tests that a custom request parameter is properly applied when spcecified\n\n        It's a bit kludgy, we check that if we specify a timeout of 0, then requests raises a ValueError, which is the\n        intended behaviour for that library given that option. That proves that the request_options are getting passed\n        properly for any given request\n        \"\"\"\n\n        geo = Geoserver(\n            GEO_URL,\n            username=os.getenv(\"GEO_USER\", \"admin\"),\n            password=os.getenv(\"GEO_PASS\", \"geoserver\"),\n            request_options={\"timeout\": 0},\n        )\n        url = \"{}/rest/about/manifest.json\".format(geo.service_url)\n        with pytest.raises(ValueError):\n            geo._requests(\"get\", url)\n\n\nclass TestGeoserverMethods:\n\n    def test_get_manifest(self):\n        \"\"\"\n        Tests that the manifest endpoint returns the proper dictionary\n        \"\"\"\n\n        response = geo.get_manifest()\n        assert len(response[\"about\"]) > 0\n\n    def test_get_version(self):\n        \"\"\"\n        Tests that the version endpoint returns a dictionary containing at least one resource called `GeoServer`\n        \"\"\"\n\n        response = geo.get_version()\n        assert \"GeoServer\" in [\n            resource[\"@name\"] for resource in response[\"about\"][\"resource\"]\n        ]\n\n    def test_get_status(self):\n        \"\"\"\n        Tests that the status endpoint returns a dictionary containing a key called `status`\n        \"\"\"\n\n        response = geo.get_status()\n        # NOT A TYPO! Geoserver returns a key called exactly `statuss`\n        assert \"statuss\" in response.keys()\n\n    def test_get_system_status(self):\n        \"\"\"\n        Tests that the status endpoint returns a dictionary containing a key called `metric`\n        \"\"\"\n\n        response = geo.get_system_status()\n        assert \"metrics\" in response.keys()\n\n    def test_reload(self):\n        \"\"\"\n        Tests that the reload endpoint returns the string `Status code: 200`\n        \"\"\"\n\n        response = geo.reload()\n        assert response == \"Status code: 200\"\n\n    def test_reset(self):\n        \"\"\"\n        Tests that the reset endpoint returns the string `Status code: 200`\n        \"\"\"\n\n        response = geo.reset()\n        assert response == \"Status code: 200\"\n\n\nclass TestWorkspace:\n\n    def test_get_default_workspace(self):\n\n        response = geo.get_default_workspace()\n        # Assuming that we are using the kartoza/geoserver docker image, which uses `ne` as the default workspace\n        assert response[\"workspace\"][\"name\"] == \"ne\"\n\n    def test_get_workspace(self):\n\n        response = geo.get_workspace(\"ne\")\n        assert response[\"workspace\"][\"name\"] == \"ne\"\n\n    def test_get_workspaces(self):\n\n        response = geo.get_workspaces()\n        # Assuming that we are using the kartoza/geoserver docker image, which uses the following as workspaces\n        expected_workspace_names = sorted(\n            [\"cite\", \"it.geosolutions\", \"ne\", \"nurc\", \"sde\", \"sf\", \"tiger\", \"topp\"]\n        )\n        for expected_workspace_name in expected_workspace_names:\n            assert expected_workspace_name in [\n                ws[\"name\"] for ws in response[\"workspaces\"][\"workspace\"]\n            ]\n\n    def test_set_default_workspace(self):\n\n        try:\n            geo.set_default_workspace(\"cite\")\n            response = geo.get_default_workspace()\n            assert response[\"workspace\"][\"name\"] == \"cite\"\n        finally:\n            # Assuming that we are using the kartoza/geoserver docker image, which uses `ne` as the default workspace\n            geo.set_default_workspace(\"ne\")\n\n\n@pytest.mark.skip(reason=\"Only setup for local testing.\")\nclass TestRequest:\n    def test_information(self):\n        geo.get_version()\n        geo.get_manifest()\n        geo.get_status()\n        geo.get_system_status()\n\n    def test_datastore_create(self):\n        a = geo.create_shp_datastore(\n            r\"C:\\Program Files (x86)\\GeoServer 2.15.1\\data_dir\\data\\demo\\C_Jamoat\\C_Jamoat.zip\",\n            store_name=\"111\",\n        )\n        # assert a == \"something we expect\"\n        print(a)\n        geo.get_layer(\"jamoat-db\", workspace=\"demo\")\n        geo.get_datastore(\"111\", \"demo\")\n        geo.get_style(\n            \"hazard_exp\",\n            workspace=\"geoinformatics_center\",\n        )\n        a = geo.get_styles()\n        # assert a == \"something we expect\"\n\n        a = geo.create_datastore(\n            \"datastore4\",\n            r\"http://localhost:8080/geoserver/wfs?request=GetCapabilities\",\n            workspace=\"demo\",\n            overwrite=True,\n        )\n        # assert a == \"something we expect\"\n\n        a = geo.create_shp_datastore(\n            r\"C:\\Users\\tek\\Desktop\\try\\geoserver-rest\\data\\A_Admin_boundaries\\A_Country\\A_Country.zip\",\n            \"aaa\",\n            \"default\",\n        )\n        # assert a == \"something we expect\"\n        print(a)\n\n        geo.publish_featurestore(\"datastore2\", \"admin_units\", workspace=\"demo\")\n\n\nclass TestCoverages:\n\n    def setup_method(self):\n        self.workspace_name = \"test_workspace\"\n        self.coveragestore = \"tos\"\n        self.coverage_name = \"tos_test\"\n        self.coverage_title = \"tos test title\"\n        self.path = f\"{HERE}/data/tos_O1_2001-2002.nc\"\n        self.type = \"NetCDF\"\n        try:\n            geo.create_workspace(self.workspace_name)\n        except:\n            geo.delete_workspace(self.workspace_name)\n            geo.create_workspace(self.workspace_name)\n        geo.create_coveragestore(\n            path=self.path,\n            workspace=self.workspace_name,\n            layer_name=self.coveragestore,\n            file_type=self.type,\n            content_type=\"application/x-netcdf\",\n            method=\"file\",\n        )\n\n    def teardown_method(self):\n        geo.delete_coveragestore(\n            coveragestore_name=self.coveragestore, workspace=self.workspace_name\n        )\n        geo.delete_workspace(self.workspace_name)\n\n    @pytest.mark.skip(reason=\"Only setup for local testing.\")\n    def test_coverage(self):\n        geo.create_coveragestore(\n            r\"C:\\Users\\tek\\Desktop\\try\\geoserver-rest\\data\\C_EAR\\a_Agriculture\\agri_final_proj.tif\",\n            workspace=\"demo\",\n            lyr_name=\"name_try\",\n            overwrite=False,\n        )\n        geo.upload_style(\n            r\"C:\\Users\\tek\\Desktop\\try_sld.sld\", sld_version=\"1.1.0\", workspace=\"try\"\n        )\n        geo.publish_style(\"agri_final_proj\", \"dem\", \"demo\")\n        color_ramp1 = {\"value1\": \"#ffff55\", \"value2\": \"#505050\", \"value3\": \"#404040\"}\n        geo.create_coveragestyle(\n            style_name=\"demo\",\n            raster_path=r\"C:\\Users\\tek\\Desktop\\try\\geoserver-rest\\data\\flood_alert.tif\",\n            workspace=\"demo\",\n            color_ramp=color_ramp1,\n            cmap_type=\"values\",\n            overwrite=True,\n        )\n\n    @pytest.mark.skip(reason=\"Only setup for local testing.\")\n    def test_create_coverage(self):\n        resp = geo.create_coverage(\n            workspace=self.workspace_name,\n            coveragestore=self.coveragestore,\n            coverage_name=self.coverage_name,\n            coverage_title=self.coverage_title,\n        )\n        assert resp == self.coverage_name\n\n\n# @pytest.mark.skip(reason=\"Only setup for local testing.\")\nclass TestFeatures:\n\n    def test_featurestore(self):\n        \"\"\"\n        Tests that you can publish an existing table as a layer\n        \"\"\"\n\n        table_name = \"test_table\"\n        workspace_name = \"test_ws\"\n        featurestore_name = \"test_ds\"\n\n        # set up DB and create a table with a feature inside\n        DB_HOST = postgis_params_local[\"host\"]\n        DB_PORT = postgis_params_local[\"port\"]\n        DB_PASS = postgis_params_local[\"pg_password\"]\n        DB_USER = postgis_params_local[\"pg_user\"]\n        DB_NAME = postgis_params_local[\"db\"]\n        engine = sa.create_engine(\n            f\"postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}\",\n            echo=False,\n        )\n        with engine.connect() as conn:\n            conn.execute(sa.text(f\"drop table if exists {table_name};\"))\n            conn.execute(\n                sa.text(\n                    f\"create table {table_name} (id integer primary key, foo text, geom geometry);\"\n                )\n            )\n            conn.execute(\n                sa.text(\n                    f\"insert into {table_name} (id, foo, geom) values (0, 'bar', ST_MakePoint(0, 0, 4326));\"\n                )\n            )\n            conn.commit()\n\n        try:\n            geo.create_workspace(workspace_name)\n            geo.create_featurestore(\n                workspace=workspace_name, store_name=featurestore_name, **postgis_params\n            )\n            geo.publish_featurestore(\n                store_name=featurestore_name,\n                pg_table=table_name,\n                workspace=workspace_name,\n            )\n\n            wfs_query = (\n                f\"{GEO_URL}/{workspace_name}/ows?\"\n                \"service=WFS&\"\n                \"version=1.0.0&\"\n                \"request=GetFeature&\"\n                f\"typeName={workspace_name}%3A{table_name}&\"\n                \"outputFormat=application%2Fjson\"\n            )\n\n            r = requests.get(wfs_query)\n            assert r.status_code == 200\n\n            data = r.json()\n            assert data[\"features\"][0][\"properties\"][\"foo\"] == \"bar\"\n\n        finally:\n            with engine.connect() as conn:\n                conn.execute(sa.text(f\"drop table {table_name};\"))\n            geo.delete_workspace(workspace_name)\n\n    def test_sql_featurestore(self):\n        \"\"\"\n        Tests that you publish an SQL query as a layer\n        \"\"\"\n\n        workspace_name = \"test_ws\"\n        featurestore_name = \"test_ds\"\n        sqlview_name = \"test_sqlview\"\n        sqlview_key_column = \"id\"\n        sqlview_geom_column = \"geom\"\n        sqlview_query = f\"select 0 as {sqlview_key_column}, 'bar' as foo, ST_MakePoint(0, 0, 4326) as {sqlview_geom_column}\"\n        wfs_query = (\n            f\"{GEO_URL}/{workspace_name}/ows?\"\n            \"service=WFS&\"\n            \"version=1.0.0&\"\n            \"request=GetFeature&\"\n            f\"typeName={workspace_name}%3A{sqlview_name}&\"\n            \"outputFormat=application%2Fjson\"\n        )\n\n        try:\n            geo.create_workspace(workspace_name)\n            geo.create_featurestore(\n                workspace=workspace_name, store_name=featurestore_name, **postgis_params\n            )\n            geo.publish_featurestore_sqlview(\n                name=sqlview_name,\n                store_name=featurestore_name,\n                sql=sqlview_query,\n                workspace=workspace_name,\n                key_column=sqlview_key_column,\n            )\n\n            r = requests.get(wfs_query)\n            assert r.status_code == 200\n\n            data = r.json()\n            assert data[\"features\"][0][\"properties\"][\"foo\"] == \"bar\"\n\n        finally:\n            geo.delete_workspace(workspace_name)\n\n    def test_parameterized_sql_featurestore(self):\n        \"\"\"\n        Tests that you can publish a parameterized SQL query as a layer\n        \"\"\"\n\n        workspace_name = \"test_ws\"\n        featurestore_name = \"test_ds\"\n        sqlview_name = \"test_parameterized_sqlview\"\n        sqlview_key_column = \"id\"\n        sqlview_geom_column = \"geom\"\n        foo_default_val = \"bar\"\n        foo_parameterized_value = \"baz\"\n        parameters = [{\"name\": \"foo\", \"defaultValue\": foo_default_val}]\n        sqlview_query = f\"select 0 as {sqlview_key_column}, '%foo%' as foo, ST_MakePoint(0, 0, 4326) as {sqlview_geom_column}\"\n        wfs_query = (\n            f\"{GEO_URL}/{workspace_name}/ows?\"\n            \"service=WFS&\"\n            \"version=1.0.0&\"\n            \"request=GetFeature&\"\n            f\"typeName={workspace_name}%3A{sqlview_name}&\"\n            \"outputFormat=application%2Fjson\"\n        )\n\n        try:\n            geo.create_workspace(workspace_name)\n            geo.create_featurestore(\n                workspace=workspace_name, store_name=featurestore_name, **postgis_params\n            )\n            geo.publish_featurestore_sqlview(\n                name=sqlview_name,\n                store_name=featurestore_name,\n                sql=sqlview_query,\n                workspace=workspace_name,\n                key_column=sqlview_key_column,\n                parameters=parameters,\n            )\n\n            # test without specifying param (should return default value)\n            r = requests.get(wfs_query)\n            assert r.status_code == 200\n            data = r.json()\n            assert data[\"features\"][0][\"properties\"][\"foo\"] == foo_default_val\n\n            # test with specifying param\n            wfs_query += f\"&viewparams=foo:{foo_parameterized_value}\"\n            r = requests.get(wfs_query)\n            assert r.status_code == 200\n            data = r.json()\n            assert data[\"features\"][0][\"properties\"][\"foo\"] == foo_parameterized_value\n\n        finally:\n            geo.delete_workspace(workspace_name)\n\n    def test_parameterized_sql_featurestore_regexp_validator(self):\n        \"\"\"\n        Tests that the parameterized SQL view layer's logic for handling regular expressing validators works as expected\n        \"\"\"\n\n        workspace_name = \"test_ws\"\n        featurestore_name = \"test_ds\"\n        sqlview_name = \"test_parameterized_sqlview\"\n        sqlview_key_column = \"id\"\n        sqlview_geom_column = \"geom\"\n        parameters = [\n            {\"name\": \"foo\", \"defaultValue\": \"baz\"},\n            {\n                \"name\": \"bar\",\n                \"defaultValue\": \"baz-\",\n                \"regexpValidator\": \"^[\\\\w\\\\d\\\\s\\\\-]+$\",\n            },\n        ]\n        sqlview_query = f\"select 0 as {sqlview_key_column}, '%foo%' as foo, '%bar%' as bar, ST_MakePoint(0, 0, 4326) as {sqlview_geom_column}\"\n\n        try:\n            geo.create_workspace(workspace_name)\n            geo.create_featurestore(\n                workspace=workspace_name, store_name=featurestore_name, **postgis_params\n            )\n            geo.publish_featurestore_sqlview(\n                name=sqlview_name,\n                store_name=featurestore_name,\n                sql=sqlview_query,\n                workspace=workspace_name,\n                key_column=sqlview_key_column,\n                parameters=parameters,\n            )\n\n            # test that adding a hyphen to foo fails because the default regexp validator forbids it\n            wfs_query = (\n                f\"{GEO_URL}/{workspace_name}/ows?\"\n                \"service=WFS&\"\n                \"version=1.0.0&\"\n                \"request=GetFeature&\"\n                f\"typeName={workspace_name}%3A{sqlview_name}&\"\n                \"outputFormat=application%2Fjson&\"\n                f\"viewparams=foo:baz-\"\n            )\n\n            r = requests.get(wfs_query)\n            # regexp validator failure still returns 200, but resultant XML indicates a Java exception\n            assert r.status_code == 200\n            assert \"java.io.IOExceptionInvalid value for parameter foo\" in r.text\n\n            # test that adding a hyphen to bar succeeds because the custom regexp validator allows it\n            wfs_query = (\n                f\"{GEO_URL}/{workspace_name}/ows?\"\n                \"service=WFS&\"\n                \"version=1.0.0&\"\n                \"request=GetFeature&\"\n                f\"typeName={workspace_name}%3A{sqlview_name}&\"\n                \"outputFormat=application%2Fjson&\"\n                f\"viewparams=bar:baz-\"\n            )\n\n            wfs_query += f\"&viewparams=bar:baz-\"\n            r = requests.get(wfs_query)\n            assert r.status_code == 200\n            data = r.json()\n            assert data[\"features\"][0][\"properties\"][\"bar\"] == \"baz-\"\n\n        finally:\n            geo.delete_workspace(workspace_name)\n\n    def test_parameterized_sql_featurestore_fails_when_integer_parameter_has_no_default_value(\n        self,\n    ):\n        \"\"\"\n        Tests that a non-string parameter in a parameterized sql view raises a descriptive error. This problem is not\n        very well documented in Geoserver but is clearly reproducible.\n        \"\"\"\n\n        workspace_name = \"test_ws\"\n        featurestore_name = \"test_ds\"\n        sqlview_name = \"test_sqlview\"\n        sqlview_key_column = \"id\"\n        sqlview_geom_column = \"geom\"\n\n        sqlview_query = f\"\"\"\n            with comparator as (\n                select 0 as id, ST_MakePoint(0, 0, 4326) as {sqlview_geom_column}\n            )\n            \n            select\n                c.id as {sqlview_key_column},\n                c.geom as {sqlview_geom_column}\n            from\n                comparator c\n            where\n                c.id = %foo%\n        \"\"\"\n\n        parameters = [{\"name\": \"foo\"}]\n\n        try:\n            geo.create_workspace(workspace_name)\n            geo.create_featurestore(\n                workspace=workspace_name, store_name=featurestore_name, **postgis_params\n            )\n\n            with pytest.raises(ValueError):\n                geo.publish_featurestore_sqlview(\n                    name=sqlview_name,\n                    store_name=featurestore_name,\n                    sql=sqlview_query,\n                    workspace=workspace_name,\n                    parameters=parameters,\n                )\n            pass\n\n        finally:\n            geo.delete_workspace(workspace_name)\n\n\n@pytest.mark.skip(reason=\"Only setup for local testing.\")\nclass TestStyles:\n    def test_styles(self):\n        geo.create_outline_featurestyle(\n            \"demo\", geom_type=\"polygon\", workspace=\"demo\", overwrite=True\n        )\n        catagorize_xml(\n            \"kamal\", [1, 2, 3, 4, 5, 6, 7], num_of_class=30, geom_type=\"line\"\n        )\n        geo.create_catagorized_featurestyle(\n            \"kamal2\", [1, 2, 3, 4, 5, 6, 7], workspace=\"demo\"\n        )\n\n\n@pytest.mark.skip(\"Doesn't work for some reason\")\nclass TestCreateGeopackageDatastore:\n\n    def test_create_geopackage_datastore_from_file(self):\n\n        geo.create_gpkg_datastore(f\"{HERE}/data/countries-test.gpkg\")\n        store = geo.get_datastore(\"countries-test\")\n        layer = geo.get_layer(\"countries-test\")\n        assert store[\"dataStore\"][\"name\"] == \"countries-test\"\n        assert layer[\"layer\"][\"name\"] == \"countries-test\"\n\n\nclass TestUploadStyles:\n\n    def test_upload_style_from_file(self):\n\n        try:\n            geo.delete_style(\"test_upload_style\")\n        except GeoserverException:\n            pass\n\n        geo.upload_style(f\"{HERE}/data/style.sld\", \"test_upload_style\")\n        style = geo.get_style(\"test_upload_style\")\n        assert style[\"style\"][\"name\"] == \"test_upload_style\"\n\n    def test_upload_style_from_malformed_file_fails(self):\n\n        try:\n            geo.delete_style(\"style_doesnt_exist\")\n        except GeoserverException:\n            pass\n\n        with pytest.raises(ValueError):\n            geo.upload_style(\n                f\"{HERE}/data/style_doesnt_exist.sld\", \"style_doesnt_exist\"\n            )\n        with pytest.raises(GeoserverException):\n            style = geo.get_style(\"style_doesnt_exist\")\n            print()\n\n    def test_upload_style_from_xml(self):\n\n        try:\n            geo.delete_style(\"test_upload_style\")\n        except GeoserverException:\n            pass\n\n        xml = open(f\"{HERE}/data/style.sld\").read()\n        geo.upload_style(xml, \"test_upload_style\")\n        style = geo.get_style(\"test_upload_style\")\n        assert style[\"style\"][\"name\"] == \"test_upload_style\"\n\n    def test_upload_style_from_malformed_xml_fails(self):\n\n        try:\n            geo.delete_style(\"style_malformed\")\n        except GeoserverException:\n            pass\n\n        xml = open(f\"{HERE}/data/style.sld\").read()[1:]\n        with pytest.raises(ValueError):\n            geo.upload_style(xml, \"style_malformed\")\n        with pytest.raises(GeoserverException):\n            style = geo.get_style(\"style_malformed\")\n\n\n@pytest.mark.skip(reason=\"Only setup for local testing.\")\nclass TestPostGres:\n    # from geo.Postgres import Db\n\n    # pg = Db(dbname=\"postgres\", user=\"postgres\", password=\"admin\", host=\"localhost\")\n\n    def test_postgres(self):\n        print(self.pg.get_columns_names(\"zones\"))\n        # assert self.pg.get_columns_names(\"zones\") == \"something we expect\"\n        print(self.pg.get_all_values(\"zones\", \"shape_area\"))\n        # assert self.pg.get_columns_names(\"zones\") == \"something we expect\"\n        self.pg.create_schema(\"kamal kshetri\")\n        a = self.pg.get_columns_names(\"jamoat-db\")\n        print(a)\n        # assert a == \"something we expect\"\n        a = self.pg.get_all_values(\"jamoat-db\", \"shape_area\")[5]\n        print(a)\n        # assert a == \"something we expect\"\n\n\n@pytest.mark.skip(reason=\"Only setup for local testing.\")\nclass TestDeletion:\n    # There needs to be a setup here first before we can delete anything\n\n    def test_delete(self):\n        geo.delete_workspace(workspace=\"demo\")\n        geo.delete_layer(layer_name=\"agri_final_proj\", workspace=\"demo\")\n        geo.delete_featurestore(featurestore_name=\"feature_store\", workspace=\"demo\")\n        geo.delete_coveragestore(coveragestore_name=\"store_name\", workspace=\"demo\")\n        geo.delete_style(style_name=\"test_style\", workspace=\"demo\")\n\n\nclass TestOther:\n    def test_classified_xml(self):\n        classified_xml(\"test\", \"test_style\", [4, 5, 3, 12], color_ramp=\"hot\")\n\n\nclass TestCoveragestore:\n    def setup_method(self):\n        self.workspace_name = \"test_workspace\"\n        self.layer_name = \"netcdf\"\n        self.path = f\"{HERE}/data/tos_O1_2001-2002.nc\"\n        self.url = \"http://localhost:8000/tos_O1_2001-2002.nc\"\n        self.type = \"NetCDF\"\n        try:\n            geo.create_workspace(workspace=self.workspace_name)\n        except:\n            geo.delete_workspace(workspace=self.workspace_name)\n            geo.create_workspace(workspace=self.workspace_name)\n\n    def teardown_method(self):\n        geo.delete_workspace(workspace=self.workspace_name)\n\n    def _verify_coveragestore(self, response):\n        \"\"\"\n        Helper method to verify coveragestore creation\n        \"\"\"\n        assert response[\"coverageStore\"][\"name\"] == self.layer_name\n        coveragestore = geo.get_coveragestore(\n            coveragestore_name=self.layer_name, workspace=self.workspace_name\n        )\n        assert coveragestore[\"coverageStore\"][\"name\"] == self.layer_name\n        assert coveragestore[\"coverageStore\"][\"type\"] == self.type\n        assert (\n            coveragestore[\"coverageStore\"][\"workspace\"][\"name\"] == self.workspace_name\n        )\n\n    def _test_create_coveragestore(self, method, path=None):\n        \"\"\"\n        Helper method to test coveragestore creation with different methods\n        \"\"\"\n        try:\n            resp = geo.create_coveragestore(\n                path=path or self.path,\n                workspace=self.workspace_name,\n                layer_name=self.layer_name,\n                file_type=self.type,\n                content_type=\"application/x-netcdf\",\n                method=method,\n            )\n            self._verify_coveragestore(resp)\n        finally:\n            geo.delete_coveragestore(\n                coveragestore_name=self.layer_name, workspace=self.workspace_name\n            )\n\n    @pytest.mark.skip(reason=\"Only setup for local testing.\")\n    def test_create_coveragestore_using_file_method(self):\n        \"\"\"\n        Tests that a coveragestore can be created using \"file\" method\n        \"\"\"\n        self._test_create_coveragestore(\"file\")\n\n    @pytest.mark.skip(reason=\"Only setup for local testing.\")\n    def test_create_coveragestore_using_external_method(self):\n        \"\"\"\n        Tests that a coveragestore can be created using \"external\" method\n        \"\"\"\n        self._test_create_coveragestore(\"external\")\n\n    @pytest.mark.skip(reason=\"Only setup for local testing.\")\n    def test_create_coveragestore_using_url_method(self):\n        \"\"\"\n        Tests that a coveragestore can be created using \"url\" method\n        \"\"\"\n        self._test_create_coveragestore(\"url\", self.url)\n"
  },
  {
    "path": "tests/test_layergroup.py",
    "content": "import os\nimport unittest\nfrom environs import Env\nimport pytest\n\nfrom unittest.mock import MagicMock, patch #allows replacing methods ans Objects by Mocks\nfrom ddt import data, ddt, unpack #allows running the same test with different parameters\n\nfrom geo.Geoserver import Geoserver\n\n@ddt\n@pytest.mark.skip(reason=\"Wrong env vars\")\nclass TestLayerGroup(unittest.TestCase):\n    \"\"\"\n    Tests all layergroup related methods of the geoserver class.\n\n    How to use:\n\n    You need to have a geoserver that you can use for testing.\n    In order to run the test, you need to create an .env file based on the .env_template.\n    Adjust the .env file with the url and login information for the server you're testing against.\n\n    You can run this test by executing:\n    python -m unittest tests.test_layergroup\n\n    The test will temporarily create a new workspace \"unittest\" on your geoserver. This workspace\n    (or any workspace of that name that previously existed on your geoserver) will be deleted\n    after the test.\n\n    The setup of this test relies on the create_workspace, delete_workspace, \n    and create_coveragestore methods.\n\n    \"\"\"\n\n    @classmethod\n    def setUpClass(cls):\n        '''\n        is run once when setting up the test class\n\n        sets up a geoserver instance, builds a workspace, \n        and uploads 2 example layers that we can later on use to build our layergroups\n        '''\n\n        env = Env()\n        env.read_env()\n\n        cls.geoserver = Geoserver(\n            os.environ[\"GEOSERVER_URL\"], \n            username= os.environ[\"GEOSERVER_USER\"],\n            password=os.environ[\"GEOSERVER_PASSWORD\"]\n            )\n\n        # set up workspace for testing\n        try:\n            cls.geoserver.create_workspace(workspace=\"unittest\")\n        except:\n            # is raised when the workspace exists already\n            cls.geoserver.delete_workspace(workspace=\"unittest\")\n            cls.geoserver.create_workspace(workspace=\"unittest\")\n\n        # upload sample layers to the testing workspace\n        # credits for the sample file: https://github.com/mommermi/geotiff_sample\n\n        cls.geoserver.create_coveragestore(\n                layer_name=\"test_layer_1\",\n                path=\"tests/data/sample_geotiff.tif\", \n                workspace=\"unittest\"\n            )\n\n        cls.geoserver.create_coveragestore(\n                layer_name=\"test_layer_2\",\n                path=\"tests/data/sample_geotiff.tif\", \n                workspace=\"unittest\"\n            )\n\n        cls.geoserver.create_coveragestore(\n                layer_name=\"test_layer_3\",\n                path=\"tests/data/sample_geotiff.tif\", \n                workspace=\"unittest\"\n            )\n\n\n    @classmethod\n    def tearDownClass(cls):\n        '''\n        is run when tearing down test class\n        '''\n        cls.geoserver.delete_workspace(workspace=\"unittest\")\n\n    def setUp(self):\n        '''\n        is run before each individual test method\n        '''\n        pass\n        \n    def tearDown(self):\n        '''\n        is run after each individual test method\n        '''\n        # delete any remaining layergroups in the unittest workspace\n        layergroups = self.geoserver.get_layergroups(workspace=\"unittest\")\n\n        if layergroups[\"layerGroups\"] == '':\n            pass\n\n        else:\n            for layer_group_info in layergroups[\"layerGroups\"][\"layerGroup\"]:\n                self.geoserver.delete_layergroup(\n                    layergroup_name = layer_group_info[\"name\"],\n                    workspace=\"unittest\"\n                )\n\n        #delete any specific testing layergroup from the global workspace\n        try:\n            self.geoserver.delete_layergroup(\"test-layergroup-name\")\n        except:\n            pass\n\n\n    @data(\n        (\"NonExistingLayerGroup\", None), #layergroup in global workspace\n        (\"NonExistingLayerGroup\", \"unittest\") #layergroup in our workspace\n        )\n    @unpack\n    def test_get_layergroup_that_doesnt_exist(self, layergroup_name, workspace):\n\n        with self.assertRaises(Exception):\n\n            self.geoserver.get_layergroup(\n                layer_name=layergroup_name,\n                workspace=workspace\n            )\n\n    @data(\n        (\"NonExistingLayerGroup\", None), #layergroup in global workspace\n        (\"NonExistingLayerGroup\", \"unittest\") #layergroup in our workspace\n        )\n    @unpack\n    def test_delete_layergroup_that_doesnt_exist(self, layergroup_name, workspace):\n\n        with self.assertRaises(Exception):\n\n            self.geoserver.delete_layergroup(\n                layergroup_name=layergroup_name,\n                workspace=workspace\n            )\n\n    @data(\n        (\n            \"test-layergroup-name\", #name\n            \"single\", #mode\n            \"test-layergroup-title\", #title\n            \"test-layergroup-abstract-text\", #abstractText\n            [\"keyword_1\", \"keyword_2\"], #keywords\n            \"unittest\" #workspace\n        ),\n        (\n            \"test-layergroup-name\", #name\n            \"single\", #mode\n            \"test-layergroup-title\", #title\n            \"test-layergroup-abstract-text\", #abstractText\n            [\"keyword_1\", \"keyword_2\"], #keywords\n            None #workspace\n        ),\n        )\n    @unpack\n    def test_create_and_get_and_delete_layergroup(self, name, mode, title, abstract_text, keywords, workspace):\n\n        self.geoserver.create_layergroup(\n            name = name,\n            mode = mode,\n            title = title,\n            abstract_text = abstract_text,\n            layers = [\"test_layer_1\", \"test_layer_2\"],\n            workspace = workspace,\n            keywords = keywords\n        )\n\n        layer_group_dict = self.geoserver.get_layergroup(\n            layer_name = name,\n            workspace=workspace,\n        )\n\n        self.assertIsInstance(layer_group_dict, dict)\n\n        self.assertEqual(\n            layer_group_dict[\"layerGroup\"][\"name\"], name\n        )\n\n        self.assertEqual(\n            layer_group_dict[\"layerGroup\"][\"mode\"], mode.upper()\n        )\n\n        self.assertEqual(\n            layer_group_dict[\"layerGroup\"][\"title\"], title\n        )\n\n        self.assertEqual(\n            layer_group_dict[\"layerGroup\"][\"abstractTxt\"], abstract_text\n        )\n\n        if workspace is not None:\n            self.assertEqual(\n                layer_group_dict[\"layerGroup\"][\"workspace\"][\"name\"],\n                workspace,\n                \"layer_group has not been assigned to the right workspace\"\n            )\n        \n        self.assertEqual(\n            len(layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"]),\n            2,\n            f'{len(layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"])} instead of 2 layers in layergroup'\n        )\n\n        self.assertEqual(\n            layer_group_dict[\"layerGroup\"][\"keywords\"][\"string\"],\n            keywords\n        )\n\n        self.geoserver.delete_layergroup(\n            layergroup_name=name,\n            workspace=workspace\n        )\n\n        with self.assertRaises(Exception) as assertion:\n            assertion.msg = \"Layer group has not been deleted properly.\"\n            self.geoserver.get_layergroup(\n            layer_name = name,\n            workspace=workspace,\n        )\n\n    @data(\"unittest\", None)\n    def test_add_layer_to_layergroup(self, workspace):\n\n        self.geoserver.create_layergroup(\n            name = \"test-layergroup-name\",\n            mode = \"single\",\n            title = \"test_layergroup_to_add\",\n            abstract_text = \"this is an abstract text\",\n            layers = [\"test_layer_1\"],\n            workspace = workspace,\n            keywords = []\n        )\n\n        layer_group_dict = self.geoserver.get_layergroup(\n            layer_name = \"test-layergroup-name\",\n            workspace=workspace,\n        )\n        \n        self.assertIsInstance(\n            layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"],\n            dict,\n            f'presumably more than 1 layer in layergroup (layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"] is list instead of dict)'\n        )\n\n        self.geoserver.add_layer_to_layergroup(\n            layergroup_name = \"test-layergroup-name\",\n            layergroup_workspace = workspace,\n            layer_name = \"test_layer_2\",\n            layer_workspace = \"unittest\"\n        )\n\n        updated_layer_group_dict = self.geoserver.get_layergroup(\n            layer_name = \"test-layergroup-name\",\n            workspace=workspace,\n        )\n        \n        self.assertEqual(\n            len(updated_layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"]),\n            2,\n            f'{len(updated_layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"])} instead of 2 layers in layergroup'\n        )\n\n        self.geoserver.add_layer_to_layergroup(\n            layergroup_name = \"test-layergroup-name\",\n            layergroup_workspace = workspace,\n            layer_name = \"test_layer_3\",\n            layer_workspace = \"unittest\"\n        )\n\n        updated_layer_group_dict = self.geoserver.get_layergroup(\n            layer_name = \"test-layergroup-name\",\n            workspace=workspace,\n        )\n        \n        self.assertEqual(\n            len(updated_layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"]),\n            3,\n            f'{len(updated_layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"])} instead of 3 layers in layergroup'\n        )\n\n    def test_add_layer_to_layergroup_that_doesnt_exist(self):\n\n        with self.assertRaises(Exception):\n\n            self.geoserver.add_layer_to_layergroup(\n                layergroup_name = \"foo\",\n                layergroup_workspace = \"unittest\",\n                layer_name = \"test_layer_2\",\n                layer_workspace = \"unittest\"\n            )\n\n    def test_add_layer_that_doesnt_exist_to_layergroup(self):\n\n        self.geoserver.create_layergroup(\n            name = \"test-layergroup-name\",\n            mode = \"single\",\n            title = \"test_layergroup_to_add\",\n            abstract_text = \"this is an abstract text\",\n            layers = [\"test_layer_1\"],\n            workspace = \"unittest\",\n            keywords = []\n        )\n\n        with self.assertRaises(Exception):\n\n            self.geoserver.add_layer_to_layergroup(\n                layergroup_name = \"test-layergroup-name\",\n                layergroup_workspace = \"unittest\",\n                layer_name = \"bar\",\n                layer_workspace = \"unittest\"\n            )\n\n    @data(\"unittest\", None)\n    def test_remove_layer_from_layergroup(self, workspace):\n\n        self.geoserver.create_layergroup(\n            name = \"test-layergroup-name\",\n            mode = \"single\",\n            title = \"test_layergroup_to_add\",\n            abstract_text = \"this is an abstract text\",\n            layers = [\"test_layer_1\", \"test_layer_2\", \"test_layer_3\"],\n            workspace = workspace,\n            keywords = []\n        )\n\n        self.geoserver.remove_layer_from_layergroup(\n            layergroup_name = \"test-layergroup-name\",\n            layergroup_workspace = workspace,\n            layer_name = \"test_layer_1\",\n            layer_workspace = \"unittest\"\n        )\n\n        updated_layer_group_dict = self.geoserver.get_layergroup(\n            layer_name = \"test-layergroup-name\",\n            workspace=workspace,\n        )\n\n        self.assertEqual(\n            len(updated_layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"]),\n            2,\n            f'{len(updated_layer_group_dict[\"layerGroup\"][\"publishables\"][\"published\"])} instead of 2 layers in layergroup'\n        )\n\n    def test_remove_layer_from_layergroup_that_doesnt_exist(self):\n\n        with self.assertRaises(Exception):\n\n            self.geoserver.remove_layer_from_layergroup(\n                layergroup_name = \"foo\",\n                layergroup_workspace = \"unittest\",\n                layer_name = \"test_layer_2\",\n                layer_workspace = \"unittest\"\n            )\n\n    def test_remove_layer_that_doesnt_exist_from_layergroup(self):\n\n        self.geoserver.create_layergroup(\n            name = \"test-layergroup-name\",\n            mode = \"single\",\n            title = \"test_layergroup_to_add\",\n            abstract_text = \"this is an abstract text\",\n            layers = [\"test_layer_1\"],\n            workspace = \"unittest\",\n            keywords = []\n        )\n\n        with self.assertRaises(Exception):\n\n            self.geoserver.remove_layer_from_layergroup(\n                layergroup_name = \"test-layergroup-name\",\n                layergroup_workspace = \"unittest\",\n                layer_name = \"bar\",\n                layer_workspace = \"unittest\"\n            )\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_path_fix.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nTest script to demonstrate the absolute path fix for create_datastore function.\n\"\"\"\n\nimport os\nimport sys\nimport tempfile\nimport unittest\nfrom geo.Geoserver import Geoserver\n\nclass TestPathConversion(unittest.TestCase):\n    \"\"\"Test cases for path conversion functionality.\"\"\"\n    \n    def setUp(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        # Initialize GeoServer connection (adjust URL as needed)\n        self.geo = Geoserver(\n            service_url=\"http://localhost:8080/geoserver\",\n            username=\"admin\",\n            password=\"geoserver\"\n        )\n        \n        # Create some temporary test paths that will work on any machine\n        self.temp_dir = tempfile.gettempdir()\n        self.workspace = \"demo\"\n        \n    def test_absolute_path_conversion(self):\n        \"\"\"Test that absolute paths are converted to relative paths when force_absolute_path=False.\"\"\"\n        # Test paths using dynamic generation\n        test_paths = [\n            # Absolute paths (Windows-style)\n            os.path.join(self.temp_dir, \"countries.shp\"),\n            os.path.join(self.temp_dir, \"data\", \"demo\", \"countries.shp\"),\n            \n            # Absolute paths (Unix/Linux-style)\n            os.path.join(\"/tmp\", \"countries.shp\"),\n            os.path.join(\"/opt\", \"geoserver\", \"data_dir\", \"data\", \"demo\", \"countries.shp\"),\n        ]\n        \n        for path in test_paths:\n            with self.subTest(path=path):\n                # Test the path conversion logic\n                if os.path.isabs(path):\n                    filename = os.path.basename(path)\n                    expected_relative_path = f\"data/{self.workspace}/{filename}\"\n                    \n                    # Test with force_absolute_path=False (should convert to relative)\n                    force_absolute_path = False\n                    if not force_absolute_path and os.path.isabs(path):\n                        converted_path = expected_relative_path\n                    else:\n                        converted_path = path\n                    \n                    # Assert that the conversion logic works correctly\n                    self.assertEqual(converted_path, expected_relative_path)\n    \n    def test_relative_paths_unchanged(self):\n        \"\"\"Test that relative paths remain unchanged.\"\"\"\n        relative_paths = [\n            \"data/demo/countries.shp\",\n            \"countries.shp\",\n            \"./data/countries.shp\"\n        ]\n        \n        for path in relative_paths:\n            with self.subTest(path=path):\n                # Relative paths should remain unchanged\n                self.assertFalse(os.path.isabs(path))\n                \n                # The conversion logic should not affect relative paths\n                force_absolute_path = False\n                if not force_absolute_path and os.path.isabs(path):\n                    converted_path = f\"data/{self.workspace}/{os.path.basename(path)}\"\n                else:\n                    converted_path = path\n                \n                self.assertEqual(converted_path, path)\n    \n    def test_http_urls_unchanged(self):\n        \"\"\"Test that HTTP URLs remain unchanged.\"\"\"\n        http_urls = [\n            \"http://localhost:8080/geoserver/wfs?request=GetCapabilities\",\n            \"https://example.com/wfs?service=WFS&version=1.0.0&request=GetCapabilities\"\n        ]\n        \n        for url in http_urls:\n            with self.subTest(url=url):\n                # HTTP URLs should remain unchanged\n                self.assertTrue(url.startswith(\"http\"))\n                \n                # The conversion logic should not affect HTTP URLs\n                if url.startswith(\"http\"):\n                    converted_url = url  # No conversion for HTTP URLs\n                else:\n                    # This branch should not be reached for HTTP URLs\n                    converted_url = url\n                \n                self.assertEqual(converted_url, url)\n    \n    def test_force_absolute_path_parameter(self):\n        \"\"\"Test the force_absolute_path parameter behavior.\"\"\"\n        absolute_path = os.path.join(self.temp_dir, \"test.shp\")\n        filename = os.path.basename(absolute_path)\n        expected_relative = f\"data/{self.workspace}/{filename}\"\n        \n        # Test with force_absolute_path=True (default, legacy behavior)\n        # This should keep the absolute path as-is\n        force_absolute_path = True\n        if not force_absolute_path and os.path.isabs(absolute_path):\n            result_path = expected_relative\n        else:\n            result_path = absolute_path\n        \n        self.assertEqual(result_path, absolute_path)\n        \n        # Test with force_absolute_path=False (new behavior)\n        # This should convert to relative path\n        force_absolute_path = False\n        if not force_absolute_path and os.path.isabs(absolute_path):\n            result_path = expected_relative\n        else:\n            result_path = absolute_path\n        \n        self.assertEqual(result_path, expected_relative)\n    \n    def test_create_datastore_path_conversion(self):\n        \"\"\"Test that create_datastore method properly handles path conversion.\"\"\"\n        # Test with absolute path and force_absolute_path=False\n        absolute_path = os.path.join(self.temp_dir, \"test.shp\")\n        filename = os.path.basename(absolute_path)\n        workspace = \"demo\"\n        \n        # Simulate the path conversion logic from create_datastore\n        force_absolute_path = False\n        if not force_absolute_path and os.path.isabs(absolute_path):\n            # Convert absolute path to relative path inline\n            relative_path = f\"data/{workspace}/{filename}\"\n            data_url = f\"<url>file:{relative_path}</url>\"\n        else:\n            # Use path as-is (could be relative or absolute)\n            data_url = f\"<url>file:{absolute_path}</url>\"\n        \n        # Assert that the conversion happened correctly\n        expected_url = f\"<url>file:data/{workspace}/{filename}</url>\"\n        self.assertEqual(data_url, expected_url)\n        \n        # Test with force_absolute_path=True (legacy behavior)\n        force_absolute_path = True\n        if not force_absolute_path and os.path.isabs(absolute_path):\n            relative_path = f\"data/{workspace}/{filename}\"\n            data_url = f\"<url>file:{relative_path}</url>\"\n        else:\n            data_url = f\"<url>file:{absolute_path}</url>\"\n        \n        # Assert that no conversion happened (legacy behavior)\n        expected_url = f\"<url>file:{absolute_path}</url>\"\n        self.assertEqual(data_url, expected_url)\n    \n    def test_http_url_handling(self):\n        \"\"\"Test that HTTP URLs are handled correctly in create_datastore.\"\"\"\n        http_url = \"http://localhost:8080/geoserver/wfs?request=GetCapabilities\"\n        \n        # Simulate the HTTP URL handling logic from create_datastore\n        if \"http://\" in http_url:\n            data_url = f\"<GET_CAPABILITIES_URL>{http_url}</GET_CAPABILITIES_URL>\"\n        else:\n            # This branch should not be reached for HTTP URLs\n            data_url = f\"<url>file:{http_url}</url>\"\n        \n        expected_url = f\"<GET_CAPABILITIES_URL>{http_url}</GET_CAPABILITIES_URL>\"\n        self.assertEqual(data_url, expected_url)\n\ndef test_path_conversion_demo():\n    \"\"\"Demonstration function showing the path conversion functionality.\"\"\"\n    \n    # Initialize GeoServer connection (adjust URL as needed)\n    geo = Geoserver(\n        service_url=\"http://localhost:8080/geoserver\",\n        username=\"admin\",\n        password=\"geoserver\"\n    )\n    \n    # Create some temporary test paths that will work on any machine\n    temp_dir = tempfile.gettempdir()\n    \n    # Test paths using dynamic generation\n    test_paths = [\n        # Absolute paths (Windows-style)\n        os.path.join(temp_dir, \"countries.shp\"),\n        os.path.join(temp_dir, \"data\", \"demo\", \"countries.shp\"),\n        \n        # Absolute paths (Unix/Linux-style)\n        os.path.join(\"/tmp\", \"countries.shp\"),\n        os.path.join(\"/opt\", \"geoserver\", \"data_dir\", \"data\", \"demo\", \"countries.shp\"),\n        \n        # Relative paths (should remain unchanged)\n        \"data/demo/countries.shp\",\n        \"countries.shp\",\n        \n        # HTTP URLs (should remain unchanged)\n        \"http://localhost:8080/geoserver/wfs?request=GetCapabilities\"\n    ]\n    \n    workspace = \"demo\"\n    \n    print(\"Testing path conversion functionality:\")\n    print(\"=\" * 50)\n    \n    for path in test_paths:\n        print(f\"\\nOriginal path: {path}\")\n        \n        # Test the path conversion logic\n        if not path.startswith(\"http\"):\n            if os.path.isabs(path):\n                filename = os.path.basename(path)\n                relative_path = f\"data/{workspace}/{filename}\"\n                print(f\"Converted to: {relative_path}\")\n            else:\n                print(f\"Already relative: {path}\")\n        else:\n            print(\"HTTP URL - no conversion needed\")\n    \n    print(\"\\n\" + \"=\" * 50)\n    print(\"Usage examples:\")\n    print(\"=\" * 50)\n    \n    # Example 1: Using relative paths (new behavior)\n    print(\"\\n1. Using relative paths (force_absolute_path=False):\")\n    print(\"geo.create_datastore(\")\n    print(\"    name='countries',\")\n    print(\"    path='path/to/your/countries.shp',  # Your actual file path\")\n    print(\"    workspace='demo',\")\n    print(\"    force_absolute_path=False  # Convert to relative paths\")\n    print(\")\")\n    print(\"# This will create: file:data/demo/countries.shp\")\n    \n    # Example 2: Using absolute paths (legacy behavior)\n    print(\"\\n2. Using absolute paths (legacy, force_absolute_path=True):\")\n    print(\"geo.create_datastore(\")\n    print(\"    name='countries',\")\n    print(\"    path='path/to/your/countries.shp',  # Your actual file path\")\n    print(\"    workspace='demo',\")\n    print(\"    force_absolute_path=True  # This is the default\")\n    print(\")\")\n    print(\"# This will create: file:path/to/your/countries.shp\")\n    \n    # Example 3: Using HTTP URLs\n    print(\"\\n3. Using HTTP URLs:\")\n    print(\"geo.create_datastore(\")\n    print(\"    name='wfs_countries',\")\n    print(\"    path='http://localhost:8080/geoserver/wfs?request=GetCapabilities',\")\n    print(\"    workspace='demo'\")\n    print(\")\")\n    print(\"# This will create: <GET_CAPABILITIES_URL>http://localhost:8080/geoserver/wfs?request=GetCapabilities</GET_CAPABILITIES_URL>\")\n    \n    # Example 4: Cross-platform path handling\n    print(\"\\n4. Cross-platform path handling:\")\n    print(\"# The function automatically detects absolute vs relative paths\")\n    print(\"# Works on Windows, Linux, and macOS\")\n    print(\"geo.create_datastore(\")\n    print(\"    name='countries',\")\n    print(\"    path=os.path.join('/path', 'to', 'your', 'data.shp'),  # Cross-platform\")\n    print(\"    workspace='demo'\")\n    print(\")\")\n\nif __name__ == \"__main__\":\n    # Run the unit tests\n    print(\"Running unit tests...\")\n    unittest.main(argv=[''], exit=False, verbosity=2)\n    \n    print(\"\\n\" + \"=\" * 80)\n    print(\"DEMONSTRATION MODE\")\n    print(\"=\" * 80)\n    \n    # Run the demonstration\n    test_path_conversion_demo() "
  }
]