├── tests ├── data │ └── warning_list.txt ├── __init__.py ├── test_names │ ├── test_area.csv │ ├── test_too_high.csv │ ├── test_too_low.csv │ ├── test_sub_content.csv │ ├── test_complete_content.csv │ └── test_empty.csv ├── test_get_items │ ├── test_sub_content.yml │ └── test_area.yml ├── conftest.py ├── test_continent.py ├── test_continent │ └── test_continent.yml ├── check_warnings.py ├── test_names.py └── test_get_items.py ├── codecov.yml ├── docs ├── _static │ ├── logo.png │ ├── custom.css │ └── custom-icon.js ├── contribute.rst ├── _template │ └── pypackage-credit.html ├── index.rst ├── conf.py └── usage.rst ├── pygaul ├── data │ └── gaul_database.parquet ├── utils.py └── __init__.py ├── .readthedocs.yaml ├── CITATION.cff ├── .devcontainer └── devcontainer.json ├── .copier-answers.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── PULL_REQUEST_TEMPLATE │ │ └── pr_template.md │ └── feature_request.md └── workflows │ ├── release.yaml │ ├── pypackage_check.yaml │ └── unit.yaml ├── AUTHORS.rst ├── LICENSE ├── .pre-commit-config.yaml ├── .gitignore ├── noxfile.py ├── pyproject.toml ├── README.rst ├── CONTRIBUTING.rst └── CODE_OF_CONDUCT.rst /tests/data/warning_list.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """make test folder a package for coverage.""" 2 | -------------------------------------------------------------------------------- /tests/test_names/test_area.csv: -------------------------------------------------------------------------------- 1 | ,gaul0_name,gaul0_code 2 | 0,Singapore,269 3 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # disable the treemap comment and report in PRs 2 | comment: false 3 | -------------------------------------------------------------------------------- /tests/test_names/test_too_high.csv: -------------------------------------------------------------------------------- 1 | ,gaul1_name,gaul1_code 2 | 0,Ang Mo Kio-Cheng San,2968 3 | -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gee-community/pygaul/main/docs/_static/logo.png -------------------------------------------------------------------------------- /tests/test_names/test_too_low.csv: -------------------------------------------------------------------------------- 1 | ,gaul2_name,gaul2_code 2 | 0,Corse-Du-Sud,135345 3 | 1,Haute-Corse,135346 4 | -------------------------------------------------------------------------------- /docs/contribute.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | .. include:: ../CONTRIBUTING.rst 5 | :start-line: 3 6 | -------------------------------------------------------------------------------- /pygaul/data/gaul_database.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gee-community/pygaul/main/pygaul/data/gaul_database.parquet -------------------------------------------------------------------------------- /tests/test_get_items/test_sub_content.yml: -------------------------------------------------------------------------------- 1 | - 2968 2 | - 2969 3 | - 2970 4 | - 2971 5 | - 2972 6 | - 2973 7 | - 2974 8 | - 2975 9 | - 2976 10 | -------------------------------------------------------------------------------- /docs/_template/pypackage-credit.html: -------------------------------------------------------------------------------- 1 |

2 | From 3 | @12rambau/pypackage 4 | 0.1.18 Copier project. 5 |

6 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Pytest session configuration.""" 2 | 3 | import pytest_gee 4 | 5 | 6 | def pytest_configure(): 7 | """Initialize GEE from service account.""" 8 | pytest_gee.init_ee_from_service_account() 9 | -------------------------------------------------------------------------------- /tests/test_names/test_sub_content.csv: -------------------------------------------------------------------------------- 1 | ,gaul1_name,gaul1_code 2 | 0,Ang Mo Kio-Cheng San,2968 3 | 1,Bukit Timah,2969 4 | 2,Central Singapore,2970 5 | 3,Hougang,2971 6 | 4,Marine Parade,2972 7 | 5,Northeast,2973 8 | 6,Potong Pasir,2974 9 | 7,Sembawang-Hong Kah,2975 10 | 8,Tanjong Pagar,2976 11 | -------------------------------------------------------------------------------- /tests/test_get_items/test_area.yml: -------------------------------------------------------------------------------- 1 | - - 103.63828400206005 2 | - 1.1640393937452072 3 | - - 104.09003995172597 4 | - 1.1640393937452072 5 | - - 104.09003995172597 6 | - 1.4712714973692553 7 | - - 103.63828400206005 8 | - 1.4712714973692553 9 | - - 103.63828400206005 10 | - 1.1640393937452072 11 | -------------------------------------------------------------------------------- /tests/test_continent.py: -------------------------------------------------------------------------------- 1 | """Tests of the continents submanagement.""" 2 | 3 | import pygaul 4 | 5 | 6 | def test_continent(data_regression): 7 | """Check that the continent are working.""" 8 | fc = pygaul.Items(name="Africa") 9 | data_regression.check(fc.aggregate_array("gaul0_name").getInfo()) 10 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.10" 9 | 10 | sphinx: 11 | configuration: docs/conf.py 12 | 13 | python: 14 | install: 15 | - method: pip 16 | path: . 17 | extra_requirements: 18 | - doc 19 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: "1.2.0" 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Rambaud" 5 | given-names: "Pierrick" 6 | orcid: "https://orcid.org/0000-0001-8764-5749" 7 | title: "pyGAUL" 8 | version: "0.4.0" 9 | doi: "" 10 | date-released: "2024-12-07" 11 | url: "https://github.com/gee-community/pygaul" 12 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python 3", 3 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", 4 | "features": { 5 | "ghcr.io/devcontainers-extra/features/nox:2": {}, 6 | "ghcr.io/devcontainers-extra/features/pre-commit:2": {} 7 | }, 8 | "postCreateCommand": "python -m pip install commitizen uv && pre-commit install" 9 | } 10 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* add dollar sign in console code-block */ 2 | div.highlight-console pre span.go::before { 3 | content: "$"; 4 | margin-right: 10px; 5 | margin-left: 5px; 6 | } 7 | 8 | /* custom colors for titles */ 9 | html[data-theme="light"] { 10 | --pst-color-primary: #4b9cd3; 11 | } 12 | 13 | html[data-theme="dark"] { 14 | --pst-color-primary: #4b9cd3; 15 | } 16 | -------------------------------------------------------------------------------- /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier 2 | _commit: 0.1.18 3 | _src_path: gh:12rambau/pypackage 4 | author_email: pierrick.rambaud49@gmail.com 5 | author_first_name: Pierrick 6 | author_last_name: Rambaud 7 | author_orcid: 0000-0001-8764-5749 8 | creation_year: "2023" 9 | github_repo_name: pygaul 10 | github_user: gee-community 11 | project_name: pyGAUL 12 | project_slug: pygaul 13 | short_description: 14 | Easy access to administrative boundary defined by FAO GAUL from 15 | Python scripts 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE/pr_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request template 3 | about: Create a pull request 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## reference the related issue 10 | 11 | PR should answer problem stated in the issue tracker. please open one before starting a PR 12 | 13 | ## description of the changes 14 | 15 | Describe the changes you propose 16 | 17 | ## mention 18 | 19 | @mentions of the person or team responsible for reviewing proposed changes 20 | 21 | ## comments 22 | 23 | any other comments we should pay attention to 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Thanks goes to these wonderful people (`emoji key `_): 2 | 3 | .. raw:: html 4 | 5 | 6 | 7 | 8 | 14 | 15 | 16 |
9 | 10 | 12rambau
11 | Pierrick Rambaud 12 |
13 |
17 | 18 | This project follows the `all-contributors `_ specification. 19 | 20 | Contributions of any kind are welcome! 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | PIP_ROOT_USER_ACTION: ignore 9 | 10 | jobs: 11 | tests: 12 | uses: ./.github/workflows/unit.yaml 13 | 14 | deploy: 15 | needs: [tests] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v5 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.11" 22 | - name: Install dependencies 23 | run: pip install twine build nox[uv] 24 | - name: update citation date 25 | run: nox -s release-date 26 | - name: Build and publish 27 | env: 28 | TWINE_USERNAME: __token__ 29 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 30 | run: python -m build && twine upload dist/* 31 | -------------------------------------------------------------------------------- /tests/test_names/test_complete_content.csv: -------------------------------------------------------------------------------- 1 | ,continent,gaul0_code,gaul0_name,gaul1_code,gaul1_name,gaul2_code,gaul2_name,iso3_code 2 | 0,asia,269,Singapore,2968,Ang Mo Kio-Cheng San,130587,Administrative Unit Not Available,SGP 3 | 1,asia,269,Singapore,2969,Bukit Timah,130588,Administrative Unit Not Available,SGP 4 | 2,asia,269,Singapore,2970,Central Singapore,130589,Administrative Unit Not Available,SGP 5 | 3,asia,269,Singapore,2971,Hougang,130590,Administrative Unit Not Available,SGP 6 | 4,asia,269,Singapore,2972,Marine Parade,130591,Administrative Unit Not Available,SGP 7 | 5,asia,269,Singapore,2973,Northeast,130592,Administrative Unit Not Available,SGP 8 | 6,asia,269,Singapore,2974,Potong Pasir,130593,Administrative Unit Not Available,SGP 9 | 7,asia,269,Singapore,2975,Sembawang-Hong Kah,130594,Administrative Unit Not Available,SGP 10 | 8,asia,269,Singapore,2976,Tanjong Pagar,130595,Administrative Unit Not Available,SGP 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pierrick Rambaud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pygaul/utils.py: -------------------------------------------------------------------------------- 1 | """Utils methods for the package.""" 2 | 3 | import os 4 | import re 5 | 6 | import ee 7 | import geetools as geetools 8 | import httplib2 9 | 10 | 11 | def initialize_documentation(): 12 | """Initialize Earth Engine Python API in the context of the Documentation build. 13 | 14 | Warning: 15 | This method is only used in the documentation build and should not be used in a production environment. 16 | ``geetools`` need to be imported prior to import this function. 17 | """ 18 | # use a saved service account key if available 19 | if "EARTHENGINE_SERVICE_ACCOUNT" in os.environ: 20 | private_key = os.environ["EARTHENGINE_SERVICE_ACCOUNT"] 21 | # small massage of the key to remove the quotes coming from RDT 22 | private_key = ( 23 | private_key[1:-1] if re.compile(r"^'[^']*'$").match(private_key) else private_key 24 | ) 25 | ee.Initialize.geetools.from_service_account(private_key) 26 | 27 | elif "EARTHENGINE_PROJECT" in os.environ: 28 | ee.Initialize(project=os.environ["EARTHENGINE_PROJECT"], http_transport=httplib2.Http()) 29 | 30 | else: 31 | raise ValueError( 32 | "EARTHENGINE_SERVICE_ACCOUNT or EARTHENGINE_PROJECT environment variable is missing" 33 | ) 34 | -------------------------------------------------------------------------------- /tests/test_continent/test_continent.yml: -------------------------------------------------------------------------------- 1 | - Algeria 2 | - Ascension, Saint Helena and Tristan da Cunha 3 | - Benin 4 | - Burkina Faso 5 | - Cabo Verde 6 | - Cameroon 7 | - Central African Republic 8 | - Chad 9 | - Congo 10 | - Côte D'Ivoire 11 | - Democratic Republic of the Congo 12 | - Egypt 13 | - Equatorial Guinea 14 | - Gabon 15 | - Gambia 16 | - Ghana 17 | - Guinea 18 | - Guinea-Bissau 19 | - Liberia 20 | - Libya 21 | - Mali 22 | - Mauritania 23 | - Morocco 24 | - Niger 25 | - Nigeria 26 | - Sao Tome And Principe 27 | - Senegal 28 | - Sierra Leone 29 | - Sudan 30 | - Togo 31 | - Tunisia 32 | - Bīr Ṭawīl 33 | - Hala'Ib Triangle 34 | - Western Sahara 35 | - Comoros 36 | - Abyei 37 | - Bassas Da India 38 | - Europa Island 39 | - Glorioso Islands 40 | - Ilemi Triangle 41 | - Juan De Nova Island 42 | - Mauritius 43 | - Mayotte 44 | - Seychelles 45 | - Tromelin Island 46 | - Angola 47 | - Botswana 48 | - Burundi 49 | - Djibouti 50 | - Eritrea 51 | - Eswatini 52 | - Ethiopia 53 | - Kenya 54 | - Lesotho 55 | - Madagascar 56 | - Malawi 57 | - Mozambique 58 | - Namibia 59 | - Rwanda 60 | - Réunion 61 | - Somalia 62 | - South Africa 63 | - South Sudan 64 | - Uganda 65 | - United Republic of Tanzania 66 | - Zambia 67 | - Zimbabwe 68 | - Bouvet Island 69 | - French Southern Territories 70 | - Heard Isl. & McDonald Is. (Aust.) 71 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: [pre-commit, commit-msg] 2 | 3 | repos: 4 | - repo: "https://github.com/commitizen-tools/commitizen" 5 | rev: "v2.18.0" 6 | hooks: 7 | - id: commitizen 8 | stages: [commit-msg] 9 | 10 | - repo: "https://github.com/kynan/nbstripout" 11 | rev: "0.5.0" 12 | hooks: 13 | - id: nbstripout 14 | stages: [pre-commit] 15 | 16 | - repo: "https://github.com/pycontribs/mirrors-prettier" 17 | rev: "v3.4.2" 18 | hooks: 19 | - id: prettier 20 | stages: [pre-commit] 21 | exclude: tests\/test_.+\. 22 | 23 | - repo: https://github.com/charliermarsh/ruff-pre-commit 24 | rev: "v0.7.0" 25 | hooks: 26 | - id: ruff 27 | stages: [pre-commit] 28 | - id: ruff-format 29 | stages: [pre-commit] 30 | 31 | - repo: https://github.com/codespell-project/codespell 32 | rev: v2.2.4 33 | hooks: 34 | - id: codespell 35 | stages: [pre-commit] 36 | additional_dependencies: 37 | - tomli 38 | 39 | # Prevent committing inline conflict markers 40 | - repo: https://github.com/pre-commit/pre-commit-hooks 41 | rev: v4.3.0 42 | hooks: 43 | - id: check-merge-conflict 44 | stages: [pre-commit] 45 | args: [--assume-in-merge] 46 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :html_theme.sidebar_secondary.remove: 2 | 3 | 4 | pyGAUL 5 | ====== 6 | 7 | .. toctree:: 8 | :hidden: 9 | 10 | usage 11 | contribute 12 | 13 | Easy access to administrative boundary defined by FAO GAUL 2015 from Python scripts. 14 | 15 | This lib provides access to FAO GAUL 2015 datasets from a Python script. it is the best boundary dataset available for GEE at this point. We provide access to The current version (2015) administrative areas till level 2. 16 | 17 | .. note:: 18 | 19 | the dataset was generated in 2015 by the Food and Alimentation Organization (FAO). It has not been updated on Google Earthengine since then. Use with caution on disputed territories. 20 | 21 | install it using either ``pip`` or ``conda``: 22 | 23 | .. code-block:: console 24 | 25 | pip install pygaul 26 | 27 | and then request area of interest from their name or GADM Id: 28 | 29 | .. code-block:: python 30 | 31 | import pygaul 32 | 33 | fc = pygaul.Items(name="Singapore", content_level=1) 34 | 35 | Documentation contents 36 | ---------------------- 37 | 38 | The documentation contains 3 main sections: 39 | 40 | .. grid:: 1 2 3 3 41 | 42 | .. grid-item:: 43 | 44 | .. card:: Usage 45 | :link: usage.html 46 | 47 | Usage and installation 48 | 49 | .. grid-item:: 50 | 51 | .. card:: Contribute 52 | :link: contribute.html 53 | 54 | Help us improve the lib. 55 | 56 | .. grid-item:: 57 | 58 | .. card:: API 59 | :link: autoapi/index.html 60 | 61 | Discover the lib API. 62 | -------------------------------------------------------------------------------- /tests/check_warnings.py: -------------------------------------------------------------------------------- 1 | """Check the warnings from doc builds.""" 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | 7 | def check_warnings(file: Path) -> int: 8 | """Check the list of warnings produced by the CI tests. 9 | 10 | Raises errors if there are unexpected ones and/or if some are missing. 11 | 12 | Args: 13 | file: the path to the generated warning.txt file from 14 | the CI build 15 | 16 | Returns: 17 | 0 if the warnings are all there 18 | 1 if some warning are not registered or unexpected 19 | """ 20 | # print some log 21 | print("\n=== Sphinx Warnings test ===\n") 22 | 23 | # find the file where all the known warnings are stored 24 | warning_file = Path(__file__).parent / "data" / "warning_list.txt" 25 | 26 | test_warnings = file.read_text().strip().split("\n") 27 | ref_warnings = warning_file.read_text().strip().split("\n") 28 | 29 | print( 30 | f'Checking build warnings in file: "{file}" and comparing to expected ' 31 | f'warnings defined in "{warning_file}"\n\n' 32 | ) 33 | 34 | # find all the missing warnings 35 | missing_warnings = [] 36 | for wa in ref_warnings: 37 | index = [i for i, twa in enumerate(test_warnings) if wa in twa] 38 | if len(index) == 0: 39 | missing_warnings += [wa] 40 | print(f"Warning was not raised: {wa}") 41 | else: 42 | test_warnings.pop(index[0]) 43 | 44 | # the remaining one are unexpected 45 | for twa in test_warnings: 46 | print(f"Unexpected warning: {twa}") 47 | 48 | # delete the tmp warnings file 49 | file.unlink() 50 | 51 | return len(missing_warnings) != 0 or len(test_warnings) != 0 52 | 53 | 54 | if __name__ == "__main__": 55 | # cast the file to path and resolve to an absolute one 56 | file = Path.cwd() / "warnings.txt" 57 | 58 | # execute the test 59 | sys.exit(check_warnings(file)) 60 | -------------------------------------------------------------------------------- /.github/workflows/pypackage_check.yaml: -------------------------------------------------------------------------------- 1 | name: template update check 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | PIP_ROOT_USER_ACTION: ignore 8 | 9 | jobs: 10 | check_version: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v5 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.10" 17 | - name: install dependencies 18 | run: pip install requests 19 | - name: get latest pypackage release 20 | id: get_latest_release 21 | run: | 22 | RELEASE=$(curl -s https://api.github.com/repos/12rambau/pypackage/releases | jq -r '.[0].tag_name') 23 | echo "latest=$RELEASE" >> $GITHUB_OUTPUT 24 | echo "latest release: $RELEASE" 25 | - name: get current pypackage version 26 | id: get_current_version 27 | run: | 28 | RELEASE=$(yq -r "._commit" .copier-answers.yml) 29 | echo "current=$RELEASE" >> $GITHUB_OUTPUT 30 | echo "current release: $RELEASE" 31 | - name: open issue 32 | if: steps.get_current_version.outputs.current != steps.get_latest_release.outputs.latest 33 | uses: rishabhgupta/git-action-issue@v2 34 | with: 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | title: "Update template to ${{ steps.get_latest_release.outputs.latest }}" 37 | body: | 38 | The package is based on the ${{ steps.get_current_version.outputs.current }} version of [@12rambau/pypackage](https://github.com/12rambau/pypackage). 39 | 40 | The latest version of the template is ${{ steps.get_latest_release.outputs.latest }}. 41 | 42 | Please consider updating the template to the latest version to include all the latest developments. 43 | 44 | Run the following code in your project directory to update the template: 45 | 46 | ``` 47 | copier update --trust --defaults --vcs-ref ${{ steps.get_latest_release.outputs.latest }} 48 | ``` 49 | 50 | > **Note** 51 | > You may need to reinstall ``copier`` and ``jinja2-time`` if they are not available in your environment. 52 | 53 | After solving the merging issues you can push back the changes to your main branch. 54 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Configuration file for the Sphinx documentation builder. 2 | 3 | This file only contains a selection of the most common options. For a full 4 | list see the documentation: 5 | https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | """ 7 | 8 | # -- Path setup ---------------------------------------------------------------- 9 | from datetime import datetime 10 | 11 | # -- Project information ------------------------------------------------------- 12 | project = "pyGAUL" 13 | author = "Pierrick Rambaud" 14 | copyright = f"2023-{datetime.now().year}, {author}" 15 | release = "0.4.0" 16 | 17 | # -- General configuration ----------------------------------------------------- 18 | extensions = [ 19 | "sphinx_copybutton", 20 | "sphinx.ext.napoleon", 21 | "sphinx.ext.viewcode", 22 | "sphinx.ext.intersphinx", 23 | "sphinx_design", 24 | "autoapi.extension", 25 | "sphinxcontrib.icon", 26 | "jupyter_sphinx", 27 | ] 28 | exclude_patterns = ["**.ipynb_checkpoints"] 29 | templates_path = ["_template"] 30 | 31 | # -- Options for HTML output --------------------------------------------------- 32 | html_theme = "pydata_sphinx_theme" 33 | html_static_path = ["_static"] 34 | html_logo = "_static/logo.png" 35 | html_theme_options = { 36 | "logo": { 37 | "text": project, 38 | }, 39 | "use_edit_page_button": True, 40 | "footer_end": ["theme-version", "pypackage-credit"], 41 | "icon_links": [ 42 | { 43 | "name": "GitHub", 44 | "url": "https://github.com/gee-community/pygaul", 45 | "icon": "fa-brands fa-github", 46 | }, 47 | { 48 | "name": "Pypi", 49 | "url": "https://pypi.org/project/pygaul/", 50 | "icon": "fa-brands fa-python", 51 | }, 52 | { 53 | "name": "Conda", 54 | "url": "https://anaconda.org/conda-forge/pygaul", 55 | "icon": "fa-custom fa-conda", 56 | "type": "fontawesome", 57 | }, 58 | ], 59 | } 60 | html_context = { 61 | "github_user": "gee-community", 62 | "github_repo": "pygaul", 63 | "github_version": "main", 64 | "doc_path": "docs", 65 | } 66 | html_css_files = ["custom.css"] 67 | 68 | # -- Options for autosummary/autodoc output ------------------------------------ 69 | autodoc_typehints = "description" 70 | autoapi_dirs = ["../pygaul"] 71 | autoapi_python_class_content = "init" 72 | autoapi_member_order = "groupwise" 73 | 74 | # -- Options for intersphinx output -------------------------------------------- 75 | intersphinx_mapping = {} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | .ruff_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | docs/api/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # system IDE 134 | .vscode/ 135 | 136 | # image tmp file 137 | *Zone.Identifier 138 | 139 | # debugging notebooks 140 | test.ipynb 141 | -------------------------------------------------------------------------------- /docs/_static/custom-icon.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Set a custom icon for pypi as it's not available in the fa built-in brands 3 | */ 4 | FontAwesome.library.add( 5 | (faListOldStyle = { 6 | prefix: "fa-custom", 7 | iconName: "conda", 8 | icon: [ 9 | 24, // viewBox width 10 | 24, // viewBox height 11 | [], // ligature 12 | "e001", // unicode codepoint - private use area 13 | "M12.045.033a12.181 12.182 0 00-1.361.078 17.512 17.513 0 011.813 1.433l.48.438-.465.45a15.047 15.048 0 00-1.126 1.205l-.178.215a8.527 8.527 0 01.86-.05 8.154 8.155 0 11-4.286 15.149 15.764 15.765 0 01-1.841.106h-.86a21.847 21.848 0 00.264 2.866 11.966 11.967 0 106.7-21.89zM8.17.678a12.181 12.182 0 00-2.624 1.275 15.506 15.507 0 011.813.43A18.551 18.552 0 018.17.678zM9.423.75a16.237 16.238 0 00-.995 1.998 16.15 16.152 0 011.605.66 6.98 6.98 0 01.43-.509c.234-.286.472-.559.716-.817A15.047 15.048 0 009.423.75zM4.68 2.949a14.969 14.97 0 000 2.336c.587-.065 1.196-.1 1.812-.107a16.617 16.617 0 01.48-1.748 16.48 16.481 0 00-2.292-.481zM3.62 3.5A11.938 11.938 0 001.762 5.88a17.004 17.004 0 011.877-.444A17.39 17.391 0 013.62 3.5zm4.406.287c-.143.437-.265.888-.38 1.347a8.255 8.255 0 011.67-.803c-.423-.2-.845-.38-1.29-.544zM6.3 6.216a14.051 14.052 0 00-1.555.108c.064.523.157 1.038.272 1.554a8.39 8.391 0 011.283-1.662zm-2.55.137a15.313 15.313 0 00-2.602.716h-.078v.079a17.104 17.105 0 001.267 2.544l.043.071.072-.049a16.309 16.31 0 011.734-1.083l.057-.035V8.54a16.867 16.868 0 01-.408-2.094v-.092zM.644 8.095l-.063.2A11.844 11.845 0 000 11.655v.209l.143-.152a17.706 17.707 0 011.584-1.447l.057-.043-.043-.064a16.18 16.18 0 01-1.025-1.87zm3.77 1.253l-.18.1c-.465.273-.93.573-1.375.889l-.065.05.05.064c.309.437.645.867.996 1.276l.137.165v-.208a8.176 8.176 0 01.364-2.15zM2.2 10.853l-.072.05a16.574 16.574 0 00-1.813 1.734l-.058.058.066.057a15.449 15.45 0 001.991 1.483l.072.05.043-.08a16.738 16.74 0 011.053-1.64v-.05l-.043-.05a16.99 16.99 0 01-1.19-1.54zm1.855 2.071l-.121.172a15.363 15.363 0 00-.917 1.433l-.043.072.071.043a16.61 16.61 0 001.562.766l.193.086-.086-.193a8.04 8.04 0 01-.66-2.172zm-3.976.48v.2a11.758 11.759 0 00.946 3.326l.078.186.072-.194a16.215 16.216 0 01.845-2l.057-.063-.064-.043a17.197 17.198 0 01-1.776-1.284zm2.543 1.805l-.035.08a15.764 15.765 0 00-.983 2.479v.08h.086a16.15 16.152 0 002.688.5l.072.007v-.086a17.562 17.563 0 01.164-2.056v-.065H4.55a16.266 16.266 0 01-1.849-.896zm2.544 1.169v.114a17.254 17.255 0 00-.151 1.828v.078h.931c.287 0 .624.014.946 0h.209l-.166-.129a8.011 8.011 0 01-1.64-1.834zm-3.29 2.1l.115.172a11.988 11.988 0 002.502 2.737l.157.129v-.201a22.578 22.58 0 01-.2-2.336v-.071h-.072a16.23 16.23 0 01-2.3-.387z", // svg path (https://simpleicons.org/icons/anaconda.svg) 14 | ], 15 | }), 16 | ); 17 | -------------------------------------------------------------------------------- /.github/workflows/unit.yaml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | workflow_call: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | env: 11 | EARTHENGINE_SERVICE_ACCOUNT: ${{ secrets.EARTHENGINE_SERVICE_ACCOUNT }} 12 | EARTHENGINE_PROJECT: ${{ secrets.EARTHENGINE_PROJECT }} 13 | FORCE_COLOR: 1 14 | PIP_ROOT_USER_ACTION: ignore 15 | 16 | jobs: 17 | lint: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v5 21 | - uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.11" 24 | - uses: pre-commit/action@v3.0.0 25 | 26 | mypy: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v5 30 | - uses: actions/setup-python@v5 31 | with: 32 | python-version: "3.11" 33 | - name: Install nox 34 | run: pip install nox[uv] 35 | - name: run mypy checks 36 | run: nox -s mypy 37 | 38 | docs: 39 | needs: [lint, mypy] 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v5 43 | - uses: actions/setup-python@v5 44 | with: 45 | python-version: "3.11" 46 | - name: Install nox 47 | run: pip install nox[uv] 48 | - name: build static docs 49 | run: nox -s docs 50 | 51 | build: 52 | needs: [lint, mypy] 53 | strategy: 54 | matrix: 55 | os: [ubuntu-latest] 56 | python-version: ["3.9", "3.10", "3.11", "3.12"] 57 | include: 58 | - os: macos-latest # macos test 59 | python-version: "3.12" 60 | - os: windows-latest # windows test 61 | python-version: "3.12" 62 | runs-on: ${{ matrix.os }} 63 | steps: 64 | - uses: actions/checkout@v5 65 | - name: Set up Python ${{ matrix.python-version }} 66 | uses: actions/setup-python@v5 67 | with: 68 | python-version: ${{ matrix.python-version }} 69 | - name: Install nox 70 | run: pip install nox[uv] 71 | - name: test with pytest 72 | run: nox -s ci-test 73 | - name: assess dead fixtures 74 | if: ${{ matrix.python-version == '3.10' }} 75 | shell: bash 76 | run: nox -s dead-fixtures 77 | - uses: actions/upload-artifact@v4 78 | if: ${{ matrix.python-version == '3.10' }} 79 | with: 80 | name: coverage 81 | path: coverage.xml 82 | 83 | coverage: 84 | needs: [build] 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@v5 88 | - uses: actions/download-artifact@v4 89 | with: 90 | name: coverage 91 | - name: codecov 92 | uses: codecov/codecov-action@v4 93 | with: 94 | token: ${{ secrets.CODECOV_TOKEN }} 95 | verbose: true 96 | fail_ci_if_error: true 97 | -------------------------------------------------------------------------------- /tests/test_names.py: -------------------------------------------------------------------------------- 1 | """Tests of the ``AdmNames`` object.""" 2 | 3 | import pytest 4 | 5 | import pygaul 6 | 7 | 8 | def test_empty(dataframe_regression): 9 | """Empty request, should list the countries.""" 10 | df = pygaul.Names() 11 | dataframe_regression.check(df) 12 | 13 | 14 | def test_duplicate_input(): 15 | """Request with too many parameters.""" 16 | with pytest.raises(Exception): 17 | pygaul.Names(name="Singapore", admin="222") 18 | 19 | 20 | def test_non_existing(): 21 | """Request non existing area.""" 22 | with pytest.raises(Exception): 23 | pygaul.Names(name="t0t0") 24 | 25 | with pytest.raises(Exception): 26 | pygaul.Names(admin="t0t0") 27 | 28 | 29 | def test_area(dataframe_regression): 30 | """Request a known.""" 31 | df = pygaul.Names(name="Singapore") 32 | dataframe_regression.check(df) 33 | 34 | 35 | def test_sub_content(dataframe_regression): 36 | """Request a sublevel.""" 37 | df = pygaul.Names(name="Singapore", content_level=1) 38 | dataframe_regression.check(df) 39 | 40 | 41 | def test_complete_content(dataframe_regression): 42 | """Request the complete hierarchy of an area.""" 43 | df = pygaul.Names(name="Singapore", content_level=1, complete=True) 44 | dataframe_regression.check(df) 45 | 46 | 47 | def test_too_high(dataframe_regression): 48 | """Request a sublevel higher than available in the area.""" 49 | with pytest.warns(UserWarning): 50 | df = pygaul.Names(admin="2968", content_level=0) 51 | dataframe_regression.check(df) 52 | 53 | 54 | def test_too_low(dataframe_regression): 55 | """Request a sublevel lower than available in the area.""" 56 | with pytest.warns(UserWarning): 57 | df = pygaul.Names(admin="3435", content_level=4) 58 | dataframe_regression.check(df) 59 | 60 | 61 | def test_case_insensitive(): 62 | """Request an area without respecting the case.""" 63 | df1 = pygaul.Names(name="Singapore") 64 | df2 = pygaul.Names(name="singaPORE") 65 | 66 | assert df1.equals(df2) 67 | 68 | 69 | def test_suggestions(): 70 | """Test that when a wrong name is given 5 options are proposed in the error message.""" 71 | expected_error = 'The requested "Franc" is not part of FAO GAUL 2024. The closest matches are: France, Franca, Ranco, Franciou, Rancul.' 72 | with pytest.raises(ValueError, match=expected_error): 73 | pygaul.Names(name="Franc") 74 | 75 | 76 | def test_get_names(): 77 | """Test that get_names still works.""" 78 | df1 = pygaul.Names(name="Singapore") 79 | 80 | with pytest.warns(DeprecationWarning): 81 | df2 = pygaul.get_names(name="Singapore") 82 | assert df1.equals(df2) 83 | 84 | 85 | def test_adm_names(): 86 | """Test that AdmNames still works.""" 87 | df1 = pygaul.Names(name="Singapore") 88 | 89 | with pytest.warns(DeprecationWarning): 90 | df2 = pygaul.AdmNames(name="Singapore") 91 | assert df1.equals(df2) 92 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """All the process that can be run using nox. 2 | 3 | The nox run are build in isolated environment that will be stored in .nox. to force the venv update, remove the .nox/xxx folder. 4 | """ 5 | 6 | import datetime 7 | import fileinput 8 | 9 | import nox 10 | 11 | nox.options.sessions = ["lint", "test", "docs", "mypy"] 12 | 13 | 14 | @nox.session(reuse_venv=True, venv_backend="uv") 15 | def lint(session: nox.Session): 16 | """Apply the pre-commits.""" 17 | session.install("pre-commit") 18 | session.run("pre-commit", "run", "--all-files", *session.posargs) 19 | 20 | 21 | @nox.session(reuse_venv=True, venv_backend="uv") 22 | def test(session: nox.Session): 23 | """Run the selected tests and report coverage in html.""" 24 | session.install("-e", ".[test]") 25 | test_files = session.posargs or ["tests"] 26 | session.run("pytest", "--cov", "--cov-report=html", *test_files) 27 | 28 | 29 | @nox.session(reuse_venv=True, name="ci-test", venv_backend="uv") 30 | def ci_test(session: nox.Session): 31 | """Run all the test and report coverage in xml.""" 32 | session.install("-e", ".[test]") 33 | session.run("pytest", "--cov", "--cov-report=xml") 34 | 35 | 36 | @nox.session(reuse_venv=True, name="dead-fixtures", venv_backend="uv") 37 | def dead_fixtures(session: nox.Session): 38 | """Check for dead fixtures within the tests.""" 39 | session.install("-e", ".[test]") 40 | session.run("pytest", "--dead-fixtures") 41 | 42 | 43 | @nox.session(reuse_venv=True, venv_backend="uv") 44 | def docs(session: nox.Session): 45 | """Build the documentation.""" 46 | build = session.posargs.pop() if session.posargs else "html" 47 | session.install("-e", ".[doc]") 48 | dst, warn = f"docs/_build/{build}", "warnings.txt" 49 | session.run("sphinx-build", "-v", "-b", build, "docs", dst, "-w", warn) 50 | session.run("python", "tests/check_warnings.py") 51 | 52 | 53 | @nox.session(name="mypy", reuse_venv=True, venv_backend="uv") 54 | def mypy(session: nox.Session): 55 | """Run a mypy check of the lib.""" 56 | # waiting for a fix to https://github.com/laurent-laporte-pro/deprecated/issues/63 57 | # so we are forced to install "types-deprecated" 58 | session.install("mypy", "types-deprecated") 59 | test_files = session.posargs or ["pygaul"] 60 | session.run("mypy", *test_files) 61 | 62 | 63 | @nox.session(reuse_venv=True, venv_backend="uv") 64 | def stubgen(session: nox.Session): 65 | """Generate stub files for the lib but requires human attention before merge.""" 66 | session.install("mypy") 67 | package = session.posargs or ["pygaul"] 68 | session.run("stubgen", "-p", package[0], "-o", "stubs", "--include-private") 69 | 70 | 71 | @nox.session(name="release-date", reuse_venv=True, venv_backend="uv") 72 | def release_date(session: nox.session): 73 | """Update the release date of the citation file.""" 74 | current_date = datetime.datetime.now().strftime("%Y-%m-%d") 75 | 76 | with fileinput.FileInput("CITATION.cff", inplace=True) as file: 77 | for line in file: 78 | if line.startswith("date-released:"): 79 | print(f'date-released: "{current_date}"') 80 | else: 81 | print(line, end="") 82 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "pygaul" 7 | version = "0.4.0" 8 | description = "Easy access to administrative boundary defined by FAO GAUL from Python scripts" 9 | keywords = [ 10 | "skeleton", 11 | "Python" 12 | ] 13 | classifiers = [ 14 | "Development Status :: 3 - Alpha", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12", 21 | ] 22 | requires-python = ">=3.9" 23 | dependencies = [ 24 | "deprecated>=1.2.14", 25 | "pandas", 26 | "earthengine-api", 27 | "pyarrow", 28 | ] 29 | 30 | [[project.authors]] 31 | name = "Pierrick Rambaud" 32 | email = "pierrick.rambaud49@gmail.com" 33 | 34 | [project.license] 35 | text = "MIT" 36 | 37 | [project.readme] 38 | file = "README.rst" 39 | content-type = "text/x-rst" 40 | 41 | [project.urls] 42 | Homepage = "https://github.com/gee-community/pygaul" 43 | 44 | [project.optional-dependencies] 45 | test = [ 46 | "pytest", 47 | "pytest-cov", 48 | "pytest-deadfixtures", 49 | "httplib2", 50 | "pytest-regressions>=2.4.3", # https://github.com/ESSS/pytest-regressions/issues/136 51 | "pytest-gee", 52 | ] 53 | doc = [ 54 | "sphinx>=6.2.1,<7", # https://github.com/pydata/pydata-sphinx-theme/issues/1404 55 | "pydata-sphinx-theme", 56 | "sphinx-copybutton", 57 | "sphinx-design", 58 | "sphinx-icon", 59 | "sphinx-autoapi", 60 | "geemap", 61 | "jupyter-sphinx!=0.4.0", # https://github.com/jupyter/jupyter-sphinx/issues/222 62 | "httplib2", 63 | "ipykernel", 64 | "geetools", 65 | ] 66 | 67 | [tool.hatch.build.targets.wheel] 68 | only-include = ["pygaul"] 69 | 70 | [tool.hatch.envs.default] 71 | dependencies = [ 72 | "pre-commit", 73 | "commitizen", 74 | "nox[uv]" 75 | ] 76 | post-install-commands = ["pre-commit install"] 77 | 78 | [tool.commitizen] 79 | tag_format = "v$major.$minor.$patch$prerelease" 80 | update_changelog_on_bump = false 81 | version = "0.4.0" 82 | version_files = [ 83 | "pyproject.toml:version", 84 | "pygaul/__init__.py:__version__", 85 | "docs/conf.py:release", 86 | "CITATION.cff:version" 87 | ] 88 | 89 | [tool.pytest.ini_options] 90 | testpaths = "tests" 91 | 92 | [tool.ruff] 93 | line-length = 100 94 | fix = true 95 | 96 | [tool.ruff.lint] 97 | select = ["E", "F", "W", "I", "D", "RUF"] 98 | ignore = [ 99 | "E501", # line too long | Black take care of it 100 | "D212", # Multi-line docstring | We use D213 101 | "D101", # Missing docstring in public class | We use D106 102 | ] 103 | 104 | [tool.ruff.lint.flake8-quotes] 105 | docstring-quotes = "double" 106 | 107 | [tool.ruff.lint.pydocstyle] 108 | convention = "google" 109 | 110 | [tool.coverage.run] 111 | source = ["pygaul"] 112 | 113 | [tool.mypy] 114 | scripts_are_modules = true 115 | ignore_missing_imports = true 116 | install_types = true 117 | non_interactive = true 118 | warn_redundant_casts = true 119 | 120 | [tool.codespell] 121 | skip = "./pygaul/data/gaul_continent.json,**/*.csv,**/*.svg" 122 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | pyGAUL 3 | ====== 4 | 5 | .. |license| image:: https://img.shields.io/badge/License-MIT-yellow.svg?logo=opensourceinitiative&logoColor=white 6 | :target: LICENSE 7 | :alt: License: MIT 8 | 9 | .. |commit| image:: https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?logo=git&logoColor=white 10 | :target: https://conventionalcommits.org 11 | :alt: conventional commit 12 | 13 | .. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 14 | :target: https://github.com/astral-sh/ruff 15 | :alt: ruff badge 16 | 17 | .. |prettier| image:: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?logo=prettier&logoColor=white 18 | :target: https://github.com/prettier/prettier 19 | :alt: prettier badge 20 | 21 | .. |pre-commmit| image:: https://img.shields.io/badge/pre--commit-active-yellow?logo=pre-commit&logoColor=white 22 | :target: https://pre-commit.com/ 23 | :alt: pre-commit 24 | 25 | .. |pypi| image:: https://img.shields.io/pypi/v/pygaul?color=blue&logo=pypi&logoColor=white 26 | :target: https://pypi.org/project/pygaul/ 27 | :alt: PyPI version 28 | 29 | .. |conda| image:: https://img.shields.io/conda/vn/conda-forge/pygaul?logo=condaforge&logoColor=white&color=orange 30 | :target: https://anaconda.org/conda-forge/pygaul 31 | :alt: conda distrib 32 | 33 | .. |build| image:: https://img.shields.io/github/actions/workflow/status/gee-community/pygaul/unit.yaml?logo=github&logoColor=white 34 | :target: https://github.com/gee-community/pygaul/actions/workflows/unit.yaml 35 | :alt: build 36 | 37 | .. |coverage| image:: https://img.shields.io/codecov/c/github/gee-community/pygaul?logo=codecov&logoColor=white 38 | :target: https://codecov.io/gh/gee-community/pygaul 39 | :alt: Test Coverage 40 | 41 | .. |docs| image:: https://img.shields.io/readthedocs/pygaul?logo=readthedocs&logoColor=white 42 | :target: https://pygaul.readthedocs.io/en/latest/ 43 | :alt: Documentation Status 44 | 45 | |license| |commit| |ruff| |prettier| |pre-commmit| |pypi| |conda| |build| |coverage| |docs| 46 | 47 | Overview 48 | -------- 49 | 50 | .. image:: docs/_static/logo.svg 51 | :width: 20% 52 | :align: right 53 | 54 | Easy access to administrative boundary defined by FAO GAUL 2015 from Python scripts. 55 | 56 | This lib provides access to FAO GAUL 2015 datasets from a Python script. it is the best boundary dataset available for GEE at this point. We provide access to The current version (2015) administrative areas till level 2. 57 | 58 | install it using either ``pip`` or ``conda``: 59 | 60 | .. code-block:: console 61 | 62 | pip install pygaul 63 | 64 | and then request area of interest from their name or GADM Id: 65 | 66 | .. code-block:: python 67 | 68 | import pygaul 69 | 70 | fc = pygaul.Items(name="Singapore", content_level=1) 71 | 72 | Note 73 | ---- 74 | 75 | the dataset was generated in 2015 by the Food and Alimentation Organization (FAO). It has not been updated on Google Earthengine since then. Use with caution on disputed territories. 76 | 77 | 78 | Credits 79 | ------- 80 | 81 | This package was created with `Copier `__ and the `@12rambau/pypackage `__ 0.1.18 project template. 82 | -------------------------------------------------------------------------------- /tests/test_get_items.py: -------------------------------------------------------------------------------- 1 | """Tests of the ``AdmItems`` function.""" 2 | 3 | import pytest 4 | 5 | import pygaul 6 | 7 | 8 | def test_empty(): 9 | """Empty request.""" 10 | with pytest.raises(Exception): 11 | pygaul.Items() 12 | 13 | 14 | def test_duplicate_intput(): 15 | """Request with too many parameters.""" 16 | # request with too many things 17 | with pytest.raises(Exception): 18 | pygaul.Items(name="Singapore", admin="222") 19 | 20 | 21 | def test_non_existing(): 22 | """Request non existing area.""" 23 | with pytest.raises(Exception): 24 | pygaul.Items(name="t0t0") 25 | 26 | with pytest.raises(Exception): 27 | pygaul.Items(admin="t0t0") 28 | 29 | 30 | def test_area(data_regression): 31 | """Request a known geometry.""" 32 | fc = pygaul.Items(name="Singapore") 33 | assert fc.size().getInfo() == 1 34 | assert fc.first().get("gaul0_code").getInfo() == 269 35 | data_regression.check(fc.geometry().bounds().coordinates().get(0).getInfo()) 36 | 37 | 38 | def test_sub_content(data_regression): 39 | """Request a sublevel.""" 40 | fc = pygaul.Items(name="Singapore", content_level=1) 41 | assert all([i == 269 for i in fc.aggregate_array("gaul0_code").getInfo()]) 42 | data_regression.check(fc.aggregate_array("gaul1_code").getInfo()) 43 | 44 | 45 | def test_too_high(): 46 | """Request a sublevel higher than available in the area.""" 47 | with pytest.warns(UserWarning): 48 | fc = pygaul.Items(admin="2968", content_level=0) 49 | assert fc.size().getInfo() == 1 50 | assert fc.aggregate_array("gaul1_code").getInfo() == [2968] 51 | 52 | 53 | def test_too_low(): 54 | """Request a sublevel lower than available in the area.""" 55 | # request a level too low 56 | with pytest.warns(UserWarning): 57 | fc = pygaul.Items(admin="2968", content_level=3) 58 | assert fc.size().getInfo() == 1 59 | assert fc.aggregate_array("gaul1_code").getInfo() == [2968] 60 | 61 | 62 | def test_case_insensitive(): 63 | """Request an area without respecting the case.""" 64 | fc1 = pygaul.Items(name="Singapore") 65 | fc2 = pygaul.Items(name="singaPORE") 66 | 67 | # just check that all ids of the fgeatures are the same as they all come from the same 68 | # initial ee.FeatureCollection 69 | ids1 = fc1.aggregate_array("system:index").sort() 70 | ids2 = fc2.aggregate_array("system:index").sort() 71 | 72 | assert ids1.equals(ids2).getInfo() 73 | 74 | 75 | def test_multiple_input(data_regression): 76 | """Test when several geometries are requested at once.""" 77 | fc1 = pygaul.Items(name=["france", "germany"]) 78 | data_regression.check(fc1.getInfo()) 79 | 80 | # just check that all ids of the features are the same as they all come from the same 81 | # initial ee.FeatureCollection 82 | fc2 = pygaul.Items(admin=["301", "303"]) 83 | ids1 = fc1.aggregate_array("system:index").sort() 84 | ids2 = fc2.aggregate_array("system:index").sort() 85 | assert ids1.equals(ids2).getInfo() 86 | 87 | 88 | def test_get_items(): 89 | """Test that get_items still works.""" 90 | fc1 = pygaul.Items(name="Singapore") 91 | ids1 = fc1.aggregate_array("system:index").sort() 92 | 93 | with pytest.warns(DeprecationWarning): 94 | fc2 = pygaul.get_items(name="Singapore") 95 | ids2 = fc2.aggregate_array("system:index").sort() 96 | assert ids1.equals(ids2).getInfo() 97 | 98 | 99 | def test_adm_items(): 100 | """Test that AdmItems still works.""" 101 | fc1 = pygaul.Items(name="Singapore") 102 | ids1 = fc1.aggregate_array("system:index").sort() 103 | 104 | with pytest.warns(DeprecationWarning): 105 | fc2 = pygaul.get_items(name="Singapore") 106 | ids2 = fc2.aggregate_array("system:index").sort() 107 | assert ids1.equals(ids2).getInfo() 108 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | Thank you for your help improving **pyGAUL**! 5 | 6 | **pyGAUL** uses `nox `__ to automate several development-related tasks. 7 | Currently, the project uses four automation processes (called sessions) in ``noxfile.py``: 8 | 9 | - ``mypy``: to perform a mypy check on the lib; 10 | - ``test``: to run the test with pytest; 11 | - ``docs``: to build the documentation in the ``build`` folder; 12 | - ``lint``: to run the pre-commits in an isolated environment 13 | 14 | Every nox session is run in its own virtual environment, and the dependencies are installed automatically. 15 | 16 | To run a specific nox automation process, use the following command: 17 | 18 | .. code-block:: console 19 | 20 | nox -s 21 | 22 | For example: ``nox -s test`` or ``nox -s docs``. 23 | 24 | Workflow for contributing changes 25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 26 | 27 | We follow a typical GitHub workflow of: 28 | 29 | - Create a personal fork of this repo 30 | - Create a branch 31 | - Open a pull request 32 | - Fix findings of various linters and checks 33 | - Work through code review 34 | 35 | See the following sections for more details. 36 | 37 | Clone the repository 38 | ^^^^^^^^^^^^^^^^^^^^ 39 | 40 | First off, you'll need your own copy of **pyGAUL** codebase. You can clone it for local development like so: 41 | 42 | Fork the repository so you have your own copy on GitHub. See the `GitHub forking guide for more information `__. 43 | 44 | Then, clone the repository locally so that you have a local copy to work on: 45 | 46 | .. code-block:: console 47 | 48 | git clone https://github.com//pygaul 49 | cd pygaul 50 | 51 | Then install the development version of the extension: 52 | 53 | .. code-block:: console 54 | 55 | pip install -e .[dev] 56 | 57 | This will install the **pyGAUL** library, together with two additional tools: 58 | - `pre-commit `__ for automatically enforcing code standards and quality checks before commits. 59 | - `nox `__, for automating common development tasks. 60 | 61 | Lastly, activate the pre-commit hooks by running: 62 | 63 | .. code-block:: console 64 | 65 | pre-commit install 66 | 67 | This will install the necessary dependencies to run pre-commit every time you make a commit with Git. 68 | 69 | Contribute to the codebase 70 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 71 | 72 | Any larger updates to the codebase should include tests and documentation. The tests are located in the ``tests`` folder, and the documentation is located in the ``docs`` folder. 73 | 74 | To run the tests locally, use the following command: 75 | 76 | .. code-block:: console 77 | 78 | nox -s test 79 | 80 | See :ref:`below ` for more information on how to update the documentation. 81 | 82 | .. _contributing-docs: 83 | 84 | Contribute to the docs 85 | ^^^^^^^^^^^^^^^^^^^^^^ 86 | 87 | The documentation is built using `Sphinx `__ and deployed to `Read the Docs `__. 88 | 89 | To build the documentation locally, use the following command: 90 | 91 | .. code-block:: console 92 | 93 | nox -s docs 94 | 95 | For each pull request, the documentation is built and deployed to make it easier to review the changes in the PR. To access the docs build from a PR, click on the "Read the Docs" preview in the CI/CD jobs. 96 | 97 | Release new version 98 | ^^^^^^^^^^^^^^^^^^^ 99 | 100 | To release a new version, start by pushing a new bump from the local directory: 101 | 102 | .. code-block:: 103 | 104 | cz bump 105 | 106 | The commitizen-tool will detect the semantic version name based on the existing commits messages. 107 | 108 | Then push to Github. In Github design a new release using the same tag name nad the ``release.yaml`` job will send it to pipy. 109 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.rst: -------------------------------------------------------------------------------- 1 | Contributor Covenant Code of Conduct 2 | ==================================== 3 | 4 | Our Pledge 5 | ---------- 6 | 7 | We as members, contributors, and leaders pledge to make participation in our 8 | community a harassment-free experience for everyone, regardless of age, body 9 | size, visible or invisible disability, ethnicity, sex characteristics, gender 10 | identity and expression, level of experience, education, socio-economic status, 11 | nationality, personal appearance, race, religion, or sexual identity 12 | and orientation. 13 | 14 | We pledge to act and interact in ways that contribute to an open, welcoming, 15 | diverse, inclusive, and healthy community. 16 | 17 | Our Standards 18 | ------------- 19 | 20 | Examples of behavior that contributes to a positive environment for our 21 | community include: 22 | 23 | * Demonstrating empathy and kindness toward other people 24 | * Being respectful of differing opinions, viewpoints, and experiences 25 | * Giving and gracefully accepting constructive feedback 26 | * Accepting responsibility and apologizing to those affected by our mistakes, 27 | and learning from the experience 28 | * Focusing on what is best not just for us as individuals, but for the 29 | overall community 30 | 31 | Examples of unacceptable behavior include: 32 | 33 | * The use of sexualized language or imagery, and sexual attention or 34 | advances of any kind 35 | * Trolling, insulting or derogatory comments, and personal or political attacks 36 | * Public or private harassment 37 | * Publishing others' private information, such as a physical or email 38 | address, without their explicit permission 39 | * Other conduct which could reasonably be considered inappropriate in a 40 | professional setting 41 | 42 | Enforcement Responsibilities 43 | ---------------------------- 44 | 45 | Community leaders are responsible for clarifying and enforcing our standards of 46 | acceptable behavior and will take appropriate and fair corrective action in 47 | response to any behavior that they deem inappropriate, threatening, offensive, 48 | or harmful. 49 | 50 | Community leaders have the right and responsibility to remove, edit, or reject 51 | comments, commits, code, wiki edits, issues, and other contributions that are 52 | not aligned to this Code of Conduct, and will communicate reasons for moderation 53 | decisions when appropriate. 54 | 55 | Scope 56 | ----- 57 | 58 | This Code of Conduct applies within all community spaces, and also applies when 59 | an individual is officially representing the community in public spaces. 60 | Examples of representing our community include using an official e-mail address, 61 | posting via an official social media account, or acting as an appointed 62 | representative at an online or offline event. 63 | 64 | Enforcement 65 | ----------- 66 | 67 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 68 | reported to the FAO team responsible for enforcement at 69 | pierrick.rambaud49@gmail.com. 70 | All complaints will be reviewed and investigated promptly and fairly. 71 | 72 | All community leaders are obligated to respect the privacy and security of the 73 | reporter of any incident. 74 | 75 | Enforcement Guidelines 76 | ---------------------- 77 | 78 | Community leaders will follow these Community Impact Guidelines in determining 79 | the consequences for any action they deem in violation of this Code of Conduct: 80 | 81 | Correction 82 | ^^^^^^^^^^ 83 | 84 | **Community Impact**: Use of inappropriate language or other behavior deemed 85 | unprofessional or unwelcome in the community. 86 | 87 | **Consequence**: A private, written warning from community leaders, providing 88 | clarity around the nature of the violation and an explanation of why the 89 | behavior was inappropriate. A public apology may be requested. 90 | 91 | Warning 92 | ^^^^^^^ 93 | 94 | **Community Impact**: A violation through a single incident or series 95 | of actions. 96 | 97 | **Consequence**: A warning with consequences for continued behavior. No 98 | interaction with the people involved, including unsolicited interaction with 99 | those enforcing the Code of Conduct, for a specified period of time. This 100 | includes avoiding interactions in community spaces as well as external channels 101 | like social media. Violating these terms may lead to a temporary or 102 | permanent ban. 103 | 104 | Temporary Ban 105 | ^^^^^^^^^^^^^ 106 | 107 | **Community Impact**: A serious violation of community standards, including 108 | sustained inappropriate behavior. 109 | 110 | **Consequence**: A temporary ban from any sort of interaction or public 111 | communication with the community for a specified period of time. No public or 112 | private interaction with the people involved, including unsolicited interaction 113 | with those enforcing the Code of Conduct, is allowed during this period. 114 | Violating these terms may lead to a permanent ban. 115 | 116 | Permanent Ban 117 | ^^^^^^^^^^^^^ 118 | 119 | **Community Impact**: Demonstrating a pattern of violation of community 120 | standards, including sustained inappropriate behavior, harassment of an 121 | individual, or aggression toward or disparagement of classes of individuals. 122 | 123 | **Consequence**: A permanent ban from any sort of public interaction within 124 | the community. 125 | 126 | Attribution 127 | ----------- 128 | 129 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 130 | version 2.0, available at 131 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 132 | 133 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 134 | enforcement ladder](https://github.com/mozilla/diversity). 135 | 136 | [homepage]: https://www.contributor-covenant.org 137 | 138 | For answers to common questions about this code of conduct, see the FAQ at 139 | https://www.contributor-covenant.org/faq. Translations are available at 140 | https://www.contributor-covenant.org/translations. 141 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Get admininstrative items 5 | ------------------------- 6 | 7 | The PyGAUL lib can be used to extract information from the FAO GAUL dataset as :code:`ee.FeatureCollection`. 8 | 9 | .. note:: 10 | 11 | :code:`ee.FeatureCollection` can easily be converted to :code:`GeoDataFrame` but if interacting with Earthengine is not the chore of your usage, have a look to `pygadm `__. It will provide accès to smaller administrative boundaries and return directly a gdf. 12 | 13 | .. important:: 14 | 15 | **PyGAUL** is not managing the connection to Google Earth Engine API. The user is responsible to set up the Initialization as he see fit. 16 | This is a feature to allow users with exotic GEE connection (e.g. service accounts) to continue use the lib without any modification. 17 | 18 | Countries 19 | ^^^^^^^^^ 20 | 21 | Using the :code:`Items` class, you can access an administrative area using either its name or its GAUL identification code. 22 | 23 | For example to extract the France geometry you can use the following code: 24 | 25 | .. jupyter-execute:: 26 | 27 | import pygaul 28 | from pygaul import utils 29 | from geemap import Map 30 | 31 | utils.initialize_documentation() 32 | 33 | fc = pygaul.Items(name="France") 34 | 35 | # display it in a map 36 | m = Map(zoom=5, center=[46.21, 2.21]) 37 | m.addLayer(fc, {"color": "red"}, "") 38 | m 39 | 40 | If you know the code of the area you try to use, you can use the GADM code instead of the name. 41 | 42 | .. jupyter-execute:: 43 | 44 | import pygaul 45 | from pygaul import utils 46 | from geemap import Map 47 | 48 | utils.initialize_documentation() 49 | 50 | fc = pygaul.Items(admin="301") 51 | 52 | # display it in a map 53 | m = Map(zoom=5, center=[46.21, 2.21]) 54 | m.addLayer(fc, {"color": "red"}, "") 55 | m 56 | 57 | Smaller admin levels 58 | ^^^^^^^^^^^^^^^^^^^^ 59 | 60 | One is not bind to only request a country, any level can be accessed using both names and/or GADM code. 61 | 62 | .. jupyter-execute:: 63 | 64 | import pygaul 65 | from pygaul import utils 66 | from geemap import Map 67 | 68 | utils.initialize_documentation() 69 | 70 | fc = pygaul.Items(name="Corse-du-Sud") 71 | 72 | # display it in a map 73 | m = Map(zoom=8, center=[41.86, 8.97]) 74 | m.addLayer(fc, {"color": "red"}, "") 75 | m 76 | 77 | .. warning:: 78 | 79 | The names of countries are all unique but not the smaller administrative layers. If you request a small area using name, make sure it's the one you are looking for before running your workflow. follow :ref:`usage:Duplication issue` for more information. 80 | 81 | Content of an admin layer 82 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 83 | 84 | Using the :code:`content_level` option, one can require smaller administrative layer than the one setup in the name. For example when you request France, by setting up the :code:`content_level` option to 2, the geodataframe will include all the department geometries. 85 | 86 | .. jupyter-execute:: 87 | 88 | import pygaul 89 | from pygaul import utils 90 | from geemap import Map 91 | 92 | utils.initialize_documentation() 93 | 94 | fc = pygaul.Items(admin="301", content_level=2) 95 | 96 | # display it in a map 97 | m = Map(zoom=5, center=[46.21, 2.21]) 98 | m.addLayer(fc, {"color": "red"}, "") 99 | m 100 | 101 | Request multiple areas at once 102 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 103 | 104 | To perform regional analysis that aggregate multiple boundaries, you can now request them at once using a list of ``name`` or a list of ``admin``. In this example we request both germany and France at once: 105 | 106 | .. jupyter-execute:: 107 | 108 | import pygaul 109 | from pygaul import utils 110 | from geemap import Map 111 | 112 | utils.initialize_documentation() 113 | 114 | fc = pygaul.Items(name=["France", "Germany"], content_level=1) 115 | 116 | # display it in a map 117 | m = Map(zoom=5, center=[48.83, 5.17]) 118 | m.addLayer(fc, {"color": "red"}, "") 119 | m 120 | 121 | Continents 122 | ^^^^^^^^^^ 123 | 124 | It's possible to request all countries from one single continent using one of the following names: 125 | 126 | - North America 127 | - South America 128 | - Antartica 129 | - Europe 130 | - Asia 131 | - Oceania 132 | - Africa 133 | 134 | .. jupyter-execute:: 135 | 136 | import pygaul 137 | from pygaul import utils 138 | from geemap import Map 139 | 140 | utils.initialize_documentation() 141 | 142 | fc = pygaul.Items(name="Europe") 143 | 144 | # display it in a map 145 | m = Map(zoom=4, center = [49.38237278700955, 31.464843750000004]) 146 | m.addLayer(fc, {"color": "red"}, "") 147 | m 148 | 149 | Find administrative names 150 | ------------------------- 151 | 152 | To get the available name and GAUL code in a administrative layer you can use the :code:`Names` class with the same parameters. Use then these names in a :code:`Items` request to get the geometry. 153 | 154 | For example to get the names and codes of all the departments in France you can run: 155 | 156 | .. jupyter-execute:: 157 | 158 | import pygaul 159 | 160 | pygaul.Names(admin="301", content_level=2) # france 161 | 162 | .. note:: 163 | 164 | If needed, one can get the names of the upper administrative layers by setting the ``complete`` parameter to ``True``. 165 | 166 | .. jupyter-execute:: 167 | 168 | import pygaul 169 | 170 | pygaul.Names(name="Auvergne-Rhône-Alpes", content_level=2, complete=True) 171 | 172 | .. note:: 173 | 174 | You can also get the list of all the country names by omitting admin and name parameters. If a level is not provided the table will only show country names but other parameters remain availables. 175 | 176 | .. code-block:: python 177 | 178 | pygaul.Names() 179 | 180 | 181 | Suggestion 182 | ---------- 183 | 184 | If you make an error when writing the name of your input, the error message will suggest 5 potential candidates in the existing names of the GADM dataset: 185 | 186 | 187 | .. jupyter-execute:: 188 | :raises: ValueError 189 | 190 | import pygaul 191 | from pygaul import utils 192 | 193 | utils.initialize_documentation() 194 | 195 | fc = pygaul.Items(name="Franc") 196 | -------------------------------------------------------------------------------- /tests/test_names/test_empty.csv: -------------------------------------------------------------------------------- 1 | ,gaul0_name,gaul0_code 2 | 0,Algeria,101 3 | 1,"Ascension, Saint Helena and Tristan da Cunha",103 4 | 2,Benin,105 5 | 3,Burkina Faso,108 6 | 4,Cabo Verde,111 7 | 5,Cameroon,112 8 | 6,Central African Republic,113 9 | 7,Chad,114 10 | 8,Congo,116 11 | 9,Côte D'Ivoire,117 12 | 10,Democratic Republic of the Congo,118 13 | 11,Egypt,120 14 | 12,Equatorial Guinea,121 15 | 13,Gabon,127 16 | 14,Gambia,128 17 | 15,Ghana,129 18 | 16,Guinea,131 19 | 17,Guinea-Bissau,132 20 | 18,Liberia,139 21 | 19,Libya,140 22 | 20,Mali,143 23 | 21,Mauritania,144 24 | 22,Morocco,147 25 | 23,Niger,150 26 | 24,Nigeria,151 27 | 25,Sao Tome And Principe,154 28 | 26,Senegal,155 29 | 27,Sierra Leone,157 30 | 28,Sudan,161 31 | 29,Togo,162 32 | 30,Tunisia,164 33 | 31,Brazil,180 34 | 32,Iraq,243 35 | 33,Jordan,247 36 | 34,Lebanon,253 37 | 35,Palestine,261 38 | 36,Saudi Arabia,266 39 | 37,Syrian Arab Republic,272 40 | 38,Türkiye,278 41 | 39,Yemen,282 42 | 40,Albania,284 43 | 41,Bosnia and Herzegovina,289 44 | 42,Bulgaria,290 45 | 43,Croatia,292 46 | 44,France,301 47 | 45,Greece,305 48 | 46,Italy,312 49 | 47,Malta,318 50 | 48,North Macedonia,322 51 | 49,Portugal,325 52 | 50,Serbia,330 53 | 51,Spain,334 54 | 52,Bīr Ṭawīl,110 55 | 53,Hala'Ib Triangle,133 56 | 54,Western Sahara,167 57 | 55,Disputed Area(xJL),239 58 | 56,Israel,244 59 | 57,Akrotiri,283 60 | 58,Andorra,285 61 | 59,Cyprus,294 62 | 60,Dekelia,296 63 | 61,Gibraltar,304 64 | 62,Holy See,307 65 | 63,Monaco,319 66 | 64,Montenegro,320 67 | 65,San Marino,329 68 | 66,Comoros,115 69 | 67,Abyei,100 70 | 68,Bassas Da India,104 71 | 69,Europa Island,125 72 | 70,Glorioso Islands,130 73 | 71,Ilemi Triangle,135 74 | 72,Juan De Nova Island,136 75 | 73,Mauritius,145 76 | 74,Mayotte,146 77 | 75,Seychelles,156 78 | 76,Tromelin Island,163 79 | 77,Angola,102 80 | 78,Botswana,106 81 | 79,Burundi,109 82 | 80,Djibouti,119 83 | 81,Eritrea,122 84 | 82,Eswatini,123 85 | 83,Ethiopia,124 86 | 84,Kenya,137 87 | 85,Lesotho,138 88 | 86,Madagascar,141 89 | 87,Malawi,142 90 | 88,Mozambique,148 91 | 89,Namibia,149 92 | 90,Rwanda,152 93 | 91,Réunion,153 94 | 92,Somalia,158 95 | 93,South Africa,159 96 | 94,South Sudan,160 97 | 95,Uganda,165 98 | 96,United Republic of Tanzania,166 99 | 97,Zambia,168 100 | 98,Zimbabwe,169 101 | 99,Bangladesh,229 102 | 100,Maldives,255 103 | 101,Myanmar,257 104 | 102,India,240 105 | 103,Lao People's Democratic Republic,252 106 | 104,Chagos Archipelagio,233 107 | 105,Indonesia,241 108 | 106,Malaysia,254 109 | 107,Thailand,275 110 | 108,Timor-Leste,276 111 | 109,Ashmore and Cartier Islands,342 112 | 110,Christmas Island,344 113 | 111,Cocos (Keeling) Islands,346 114 | 112,Australia,343 115 | 113,Arunachal Pradesh,226 116 | 114,Brunei Darussalam,231 117 | 115,Armenia,225 118 | 116,Turkmenistan,277 119 | 117,Mongolia,256 120 | 118,Sri Lanka,271 121 | 119,Afghanistan,223 122 | 120,Azerbaijan,227 123 | 121,Bhutan,230 124 | 122,Cambodia,232 125 | 123,China,234 126 | 124,Democratic People's Republic of Korea,237 127 | 125,Iran (Islamic Republic Of),242 128 | 126,Japan,246 129 | 127,Kazakhstan,248 130 | 128,Kyrgyzstan,251 131 | 129,Nepal,258 132 | 130,Oman,259 133 | 131,Pakistan,260 134 | 132,Philippines,263 135 | 133,Qatar,264 136 | 134,Tajikistan,274 137 | 135,Uzbekistan,280 138 | 136,Viet Nam,281 139 | 137,Finland,300 140 | 138,Sweden,336 141 | 139,Georgia,302 142 | 140,Norway,323 143 | 141,Romania,327 144 | 142,Russian Federation,328 145 | 143,Ukraine,338 146 | 144,Aksai Chin,224 147 | 145,Bahrain,228 148 | 146,Disputed Area(xxx),238 149 | 147,"China, Hong Kong SAR",235 150 | 148,Jammu And Kashmir,245 151 | 149,Kuwait,250 152 | 150,"China, Macao SAR",236 153 | 151,Paracel Islands,262 154 | 152,Republic Of Korea,265 155 | 153,Scarborough Reef,267 156 | 154,Senkaku Islands,268 157 | 155,Singapore,269 158 | 156,Spratly Islands,270 159 | 157,Taiwan Province of China,273 160 | 158,United Arab Emirates,279 161 | 159,Republic of Moldova,326 162 | 160,Svalbard and Jan Mayen Islands,335 163 | 161,Palau,360 164 | 162,Austria,286 165 | 163,Belarus,287 166 | 164,Belgium,288 167 | 165,Czechia,295 168 | 166,Denmark,297 169 | 167,Estonia,298 170 | 168,Germany,303 171 | 169,Hungary,308 172 | 170,Latvia,314 173 | 171,Liechtenstein,315 174 | 172,Lithuania,316 175 | 173,Luxembourg,317 176 | 174,Netherlands (Kingdom of the),321 177 | 175,Poland,324 178 | 176,Slovakia,332 179 | 177,Slovenia,333 180 | 178,Switzerland,337 181 | 179,United Kingdom of Great Britain and Northern Ireland,339 182 | 180,Åland Islands,340 183 | 181,Canada,182 184 | 182,United States of America,220 185 | 183,Iceland,309 186 | 184,Ireland,310 187 | 185,Fiji,348 188 | 186,Kiribati,351 189 | 187,Micronesia (Federated States of),353 190 | 188,New Zealand,356 191 | 189,Papua New Guinea,361 192 | 190,Vanuatu,369 193 | 191,Saint Pierre and Miquelon,213 194 | 192,Kuril Islands,249 195 | 193,Faroe Islands,299 196 | 194,Guernsey,306 197 | 195,Isle of Man,311 198 | 196,Jersey,313 199 | 197,Guam,350 200 | 198,Marshall Islands,352 201 | 199,New Caledonia,355 202 | 200,Nauru,354 203 | 201,Norfolk Island,358 204 | 202,Northern Mariana Islands,359 205 | 203,Solomon Islands,364 206 | 204,Tuvalu,367 207 | 205,United States Minor Outlying Islands,368 208 | 206,Greenland,372 209 | 207,Bahamas,174 210 | 208,Cuba,187 211 | 209,El Salvador,191 212 | 210,Guatemala,196 213 | 211,Mexico,202 214 | 212,American Samoa,341 215 | 213,Clipperton Island,345 216 | 214,Cook Islands,347 217 | 215,French Polynesia,349 218 | 216,Niue,357 219 | 217,Samoa,363 220 | 218,Tokelau,365 221 | 219,Tonga,366 222 | 220,Wallis and Futuna Islands,370 223 | 221,Argentina,172 224 | 222,Belize,176 225 | 223,Bermuda,177 226 | 224,Bolivia (Plurinational State of),178 227 | 225,Chile,184 228 | 226,Colombia,185 229 | 227,Costa Rica,186 230 | 228,Dominican Republic,189 231 | 229,Ecuador,190 232 | 230,French Guiana,193 233 | 231,Guadeloupe,195 234 | 232,Guyana,197 235 | 233,Haiti,198 236 | 234,Honduras,199 237 | 235,Martinique,201 238 | 236,Nicaragua,204 239 | 237,Panama,205 240 | 238,Paraguay,206 241 | 239,Peru,207 242 | 240,Puerto Rico,208 243 | 241,Saint Lucia,211 244 | 242,Suriname,216 245 | 243,United States Virgin Islands,219 246 | 244,Uruguay,221 247 | 245,Venezuela (Bolivarian Republic Of),222 248 | 246,Anguilla,170 249 | 247,Antigua And Barbuda,171 250 | 248,Aruba,173 251 | 249,Barbados,175 252 | 250,"Bonaire, Sint Eustatius And Saba",179 253 | 251,British Virgin Islands,181 254 | 252,Cayman Islands,183 255 | 253,Dominica,188 256 | 254,Grenada,194 257 | 255,Jamaica,200 258 | 256,Montserrat,203 259 | 257,Saint Barthélemy,209 260 | 258,Saint Kitts And Nevis,210 261 | 259,Saint Martin (French part),212 262 | 260,Saint Vincent and the Grenadines,214 263 | 261,Trinidad And Tobago,217 264 | 262,Turks And Caicos Islands,218 265 | 263,Curaçao,293 266 | 264,Sint Maarten (Dutch part),331 267 | 265,Bouvet Island,107 268 | 266,French Southern Territories,126 269 | 267,Heard Isl. & McDonald Is. (Aust.),134 270 | 268,Falkland Islands (Malvinas),192 271 | 269,South Georgia and the South Sandwich Islands,215 272 | 270,Pitcairn,362 273 | 271,Antarctica,371 274 | -------------------------------------------------------------------------------- /pygaul/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Easy access to administrative boundary defined by FAO GAUL 2015 from Python scripts. 3 | 4 | This lib provides access to FAO GAUL 2015 datasets from a Python script. it is the best boundary dataset available for GEE at this point. We provide access to The current version (2015) administrative areas till level 2. 5 | """ 6 | 7 | import warnings 8 | from difflib import get_close_matches 9 | from functools import lru_cache 10 | from itertools import product 11 | from pathlib import Path 12 | from typing import List, Union 13 | 14 | import ee 15 | import numpy as np 16 | import pandas as pd 17 | from deprecated.sphinx import deprecated, versionadded # type: ignore [import-untyped] 18 | 19 | __version__ = "0.4.0" 20 | __author__ = "Pierrick Rambaud" 21 | __email__ = "pierrick.rambaud49@gmail.com" 22 | 23 | __gaul_data__ = Path(__file__).parent / "data" / "gaul_database.parquet" 24 | __gaul_asset__ = "projects/sat-io/open-datasets/FAO/GAUL/GAUL_2024_L{}" 25 | 26 | 27 | @lru_cache(maxsize=1) 28 | def _df() -> pd.DataFrame: 29 | """Get the parquet database.""" 30 | return pd.read_parquet(__gaul_data__) 31 | 32 | 33 | @versionadded(version="0.3.1", reason="Add a Names object to handle names") 34 | class Names(pd.DataFrame): 35 | def __init__( 36 | self, 37 | name: str = "", 38 | admin: str = "", 39 | content_level: int = -1, 40 | complete: bool = False, 41 | ): 42 | """Object to handle names of administrative layer using the name or the administrative code. 43 | 44 | Compute a pandas DataFrame of the names as FAO GAUL codes of and administrative region. The region can be requested either by its "name" or its "admin", the lib will identify the corresponding level on the fly. The user can also request for a specific level for its content e.g. get all admin level 1 of a country. If nothing is set we will infer the level of the item and if the level is higher than the found item, it will be ignored. If Nothing is found the method will raise an error. 45 | 46 | Args: 47 | name: The name of a administrative area. Cannot be set along with :code:`admin`. 48 | admin: The id of an administrative area in the FAO GAUL nomenclature. Cannot be set along with :code:`name`. 49 | content_level: The level to use in the final dataset. Default to -1 (use level of the selected area). 50 | complete: If True, the method will return all the names of the higher administrative areas. Default to False. 51 | """ 52 | # sanitary check on parameters 53 | if name and admin: 54 | raise ValueError('"name" and "id" cannot be set at the same time.') 55 | 56 | # if a name or admin number is set, we need to filter the dataset accordingly 57 | # if not we will simply consider the world dataset 58 | df = _df() 59 | if name or admin: 60 | # set the id we look for and tell the function if its a name or an admin 61 | is_name = True if name else False 62 | id = name if name else admin 63 | 64 | # read the data and find if the element exist 65 | column = "gaul{}_name" if is_name else "gaul{}_code" 66 | is_in = ( 67 | df.filter([column.format(i) for i in range(3)]) 68 | .apply(lambda col: col.str.lower()) 69 | .isin([id.lower()]) 70 | ) 71 | 72 | if not is_in.any().any(): 73 | # find the 5 closest names/id 74 | columns = [df[column.format(i)].dropna().str.lower().values for i in range(3)] 75 | ids = np.unique(np.concatenate(columns)) 76 | close_ids = get_close_matches(id.lower(), ids, n=5) 77 | if is_name is True: 78 | close_ids = [i.capitalize() for i in close_ids] 79 | else: 80 | close_ids = [i.upper() for i in close_ids] 81 | raise ValueError( 82 | f'The requested "{id}" is not part of FAO GAUL 2024. The closest ' 83 | f'matches are: {", ".join(close_ids)}.' 84 | ) 85 | 86 | # Get the code of the associated country of the identifed area and the associated level 87 | line = is_in[~((~is_in).all(axis=1))].idxmax(1) 88 | level = line.iloc[0][4] 89 | 90 | # load the max_level available in the requested area 91 | sub_df = df[df[column.format(level)].str.fullmatch(id, case=False)] 92 | max_level = next(i for i in reversed(range(3)) if (sub_df[f"gaul{i}_name"] != "").any()) 93 | 94 | # get the request level from user 95 | content_level, level = int(content_level), int(level) 96 | if content_level == -1: 97 | content_level = level 98 | elif content_level < level: 99 | warnings.warn( 100 | f"The requested level ({content_level}) is higher than the area ({level}). " 101 | f"Fallback to {level}." 102 | ) 103 | content_level = level 104 | 105 | if content_level > max_level: 106 | warnings.warn( 107 | f"The requested level ({content_level}) is higher than the max level " 108 | f"in this country ({max_level}). Fallback to {max_level}." 109 | ) 110 | content_level = max_level 111 | 112 | else: # no admin and no name 113 | sub_df = df 114 | content_level = 0 if content_level == -1 else content_level 115 | 116 | # get the columns name corresponding to the requested level 117 | columns = [f"gaul{content_level}_name", f"gaul{content_level}_code"] 118 | 119 | # the list will contain duplicate as all the smaller admin level will be included 120 | sub_df = sub_df.drop_duplicates(subset=columns, ignore_index=True) 121 | 122 | # the list will contain NA as all the bigger admin level will be selected as well 123 | # the database is read as pure string so dropna cannot be used 124 | # .astype is also a vectorized operation so it goes very fast 125 | sub_df = sub_df[sub_df[columns[0]].astype(bool)] 126 | 127 | # filter the df if complete is set to False, the only displayed columns will be the one requested 128 | final_df = sub_df if complete is True else sub_df[columns] 129 | 130 | super().__init__(final_df) 131 | 132 | 133 | @versionadded(version="0.3.1", reason="Add an Items class to handle admin items") 134 | class Items(ee.FeatureCollection): 135 | def __init__( 136 | self, 137 | name: Union[str, List[str]] = "", 138 | admin: Union[str, List[str]] = "", 139 | content_level: int = -1, 140 | ): 141 | """Object to handle administrative boundaries using the name or the administrative code. 142 | 143 | Return an ee.FeatureCollection representing an administrative region. The region can be requested either by its "name" or its "admin", the lib will identify the area level on the fly. The user can also request for a specific level for the GeoDataFrame features e.g. get all admin level 1 of a country. If nothing is set we will infer the level of the item and if the level is higher than the found item, it will be ignored. If Nothing is found the method will return an error. 144 | 145 | Args: 146 | name: The name of an administrative area. Cannot be set along with :code:`admin`. it can be a list or a single name. 147 | admin: The id of an administrative area in the GADM nomenclature. Cannot be set along with :code:`name`. It can be a list or a single admin code. 148 | content_level: The level to use in the final dataset. Default to -1 (use level from the area). 149 | """ 150 | # set up the loop 151 | names = [name] if isinstance(name, str) else name 152 | admins = [admin] if isinstance(admin, str) else admin 153 | 154 | # check that they are not all empty 155 | if names == [""] == admins: 156 | raise ValueError('at least "name" or "admin" need to be set.') 157 | 158 | # special parsing for continents. They are associated to the countries by FAO. 159 | continents = _df().continent.unique() 160 | if len(names) == 1 and (c := names[0].lower()) in continents: 161 | admins = [a for a in _df()[_df().continent == c].gaul0_code.unique()] 162 | names = [""] 163 | 164 | # use itertools, normally one of them is empty so it will raise an error 165 | # if not the case as admin and name will be set together 166 | fc_list = [self._items(n, a, content_level) for n, a in product(names, admins)] 167 | 168 | # concat all the data 169 | feature_collection = fc_list[0] 170 | if len(fc_list) > 1: 171 | for fc in fc_list[1:]: 172 | feature_collection = feature_collection.merge(fc) 173 | 174 | super().__init__(feature_collection) 175 | 176 | def _items( 177 | self, name: str = "", admin: str = "", content_level: int = -1 178 | ) -> ee.FeatureCollection: 179 | """ 180 | Return the requested administrative boundaries from a single name or administrative code. 181 | 182 | Args: 183 | name: The name of an administrative area. Cannot be set along with :code:`admin`. 184 | admin: The id of an administrative area in the FAO GAUL nomenclature. Cannot be set along with :code:`name`. 185 | content_level: The level to use in the final dataset. Default to -1 (use level from the area). 186 | 187 | Returns: 188 | The FeatureCollection of the requested area with all the GAUL attributes. 189 | """ 190 | # call to Names without level to raise an error if the requested level won't work 191 | df = Names(name, admin) 192 | if len(df) > 1: 193 | raise ValueError( 194 | f'The requested name ("{name}") is not unique ({len(df)} results). ' 195 | f"To retrieve it, please use the `admin` parameter instead. " 196 | f"If you don't know the GAUL code, use the following code, " 197 | f'it will return the GAUL codes as well:\n`Names(name="{name}")`' 198 | ) 199 | df.columns[0][4] 200 | 201 | # now load the useful one to get content_level 202 | df = Names(name, admin, content_level) 203 | content_level = df.columns[1][4] 204 | 205 | # checks have already been performed in Names and there should 206 | # be one single result 207 | ids = [int(v) for v in df[f"gaul{content_level}_code"].to_list()] 208 | 209 | # read the accurate dataset 210 | feature_collection = ee.FeatureCollection(__gaul_asset__.format(content_level)).filter( 211 | ee.Filter.inList(f"gaul{content_level}_code", ids) 212 | ) 213 | 214 | return feature_collection 215 | 216 | 217 | @deprecated(version="0.3.1", reason="Use the Names object instead") 218 | class AdmNames(Names): 219 | pass 220 | 221 | 222 | @deprecated(version="0.3.1", reason="Use the Items class instead") 223 | class AdmItems(Items): 224 | pass 225 | 226 | 227 | @deprecated(version="0.3.0", reason="Use the Names object instead") 228 | def get_names( 229 | name: str = "", admin: str = "", content_level: int = -1, complete: bool = False 230 | ) -> pd.DataFrame: 231 | """Return the list of names available in a administrative layer using the name or the administrative code.""" 232 | return Names(name, admin, content_level, complete) 233 | 234 | 235 | @deprecated(version="0.3.0", reason="Use the Items class instead") 236 | def get_items( 237 | name: Union[str, List[str]] = "", 238 | admin: Union[str, List[str]] = "", 239 | content_level: int = -1, 240 | ) -> ee.FeatureCollection: 241 | """Return the requested administrative boundaries using the name or the administrative code.""" 242 | return Items(name, admin, content_level) 243 | --------------------------------------------------------------------------------