├── 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 |
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 |
--------------------------------------------------------------------------------