pax_global_header00006660000000000000000000000064150062233040014505gustar00rootroot0000000000000052 comment=32e37c8a180194607926ee1f99af66bb8e9b30ec geojson-pydantic-2.0.0/000077500000000000000000000000001500622330400147615ustar00rootroot00000000000000geojson-pydantic-2.0.0/.github/000077500000000000000000000000001500622330400163215ustar00rootroot00000000000000geojson-pydantic-2.0.0/.github/codecov.yml000066400000000000000000000002041500622330400204620ustar00rootroot00000000000000comment: off coverage: status: project: default: target: auto threshold: 5 geojson-pydantic-2.0.0/.github/workflows/000077500000000000000000000000001500622330400203565ustar00rootroot00000000000000geojson-pydantic-2.0.0/.github/workflows/ci.yml000066400000000000000000000045641500622330400215050ustar00rootroot00000000000000name: CI on: push: branches: - main tags: - '*' pull_request: env: LATEST_PY_VERSION: '3.13' jobs: tests: runs-on: ubuntu-latest strategy: matrix: python-version: - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' # - '3.14.0-alpha.2', waiting for shapely to support 3.14 steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install .["test"] - name: Run pre-commit if: ${{ matrix.python-version == env.LATEST_PY_VERSION }} run: | python -m pip install pre-commit pre-commit run --all-files - name: Run tests run: python -m pytest --cov geojson_pydantic --cov-report xml --cov-report term-missing - name: Upload Results if: ${{ matrix.python-version == env.LATEST_PY_VERSION }} uses: codecov/codecov-action@v1 with: file: ./coverage.xml flags: unittests name: ${{ matrix.python-version }} fail_ci_if_error: false publish: needs: [tests] runs-on: ubuntu-latest if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ env.LATEST_PY_VERSION }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flit python -m pip install . - name: Set tag version id: tag run: | echo "version=${GITHUB_REF#refs/*/}" echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - name: Set module version id: module run: | echo version=$(python -c'import geojson_pydantic; print(geojson_pydantic.__version__)') >> $GITHUB_OUTPUT - name: Build and publish if: ${{ steps.tag.outputs.version }} == ${{ steps.module.outputs.version}} env: FLIT_USERNAME: ${{ secrets.PYPI_USERNAME }} FLIT_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: flit publish geojson-pydantic-2.0.0/.github/workflows/deploy_mkdocs.yml000066400000000000000000000013441500622330400237370ustar00rootroot00000000000000name: Publish docs via GitHub Pages on: push: branches: - main paths: # Only rebuild website when docs have changed - 'README.md' - 'CHANGELOG.md' - 'CONTRIBUTING.md' - 'docs/**' jobs: build: name: Deploy docs runs-on: ubuntu-latest steps: - name: Checkout main uses: actions/checkout@v2 - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install numpy python -m pip install .["docs"] - name: Deploy docs run: mkdocs gh-deploy --force -f docs/mkdocs.yml geojson-pydantic-2.0.0/.gitignore000066400000000000000000000022251500622330400167520ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .pytest_cache geojson-pydantic-2.0.0/.pre-commit-config.yaml000066400000000000000000000012711500622330400212430ustar00rootroot00000000000000repos: - repo: https://github.com/abravalheri/validate-pyproject rev: v0.12.1 hooks: - id: validate-pyproject - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - id: isort language_version: python - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.3.5 hooks: - id: ruff args: ["--fix"] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.11.2 hooks: - id: mypy language_version: python # No reason to run if only tests have changed. They intentionally break typing. exclude: tests/.* additional_dependencies: - pydantic~=2.0 geojson-pydantic-2.0.0/CHANGELOG.md000066400000000000000000000433061500622330400166000ustar00rootroot00000000000000 # Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). Note: Minor version `0.X.0` update might break the API, It's recommended to pin geojson-pydantic to minor version: `geojson-pydantic>=0.6,<0.7` ## [unreleased] ## [2.0.0] - 2025-05-05 * remove custom `__iter__`, `__getitem__` and `__len__` methods from `GeometryCollection` class **breaking change** ```python from geojson_pydantic.geometries import GeometryCollection, Point, MultiPoint geoms = GeometryCollection( type="GeometryCollection", geometries=[ Point(type="Point", coordinates=(102.0, 0.5)), MultiPoint(type="MultiPoint", coordinates=[(100.0, 0.0), (101.0, 1.0)]), ], ) ######## # Before for geom in geom: # __iter__ pass assert len(geoms) == 2 # __len__ _ = geoms[0] # __getitem__ ##### # Now for geom in geom.iter(): # __iter__ pass assert geoms.length == 2 # __len__ _ = geoms.geometries[0] # __getitem__ ``` * remove custom `__iter__`, `__getitem__` and `__len__` methods from `FeatureCollection` class **breaking change** ```python from geojson_pydantic import FeatureCollection, Feature, Point fc = FeatureCollection( type="FeatureCollection", features=[ Feature(type="Feature", geometry=Point(type="Point", coordinates=(102.0, 0.5)), properties={"name": "point1"}), Feature(type="Feature", geometry=Point(type="Point", coordinates=(102.0, 1.5)), properties={"name": "point2"}), ] ) ######## # Before for feat in fc: # __iter__ pass assert len(fc) == 2 # __len__ _ = fc[0] # __getitem__ ##### # Now for feat in fc.iter(): # __iter__ pass assert fc.length == 2 # __len__ _ = fe.features[0] # __getitem__ ``` * make sure `GeometryCollection` are homogeneous for Z coordinates ```python from geojson_pydantic.geometries import Point, LineString, GeometryCollection # Before GeometryCollection( type="GeometryCollection", geometries=[ Point(type="Point", coordinates=[0, 0]), # 2D point LineString( type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] # 3D LineString ), ], ) >>> GeometryCollection(bbox=None, type='GeometryCollection', geometries=[Point(bbox=None, type='Point', coordinates=Position3D(longitude=0.0, latitude=0.0, altitude=0.0)), LineString(bbox=None, type='LineString', coordinates=[Position3D(longitude=0.0, latitude=0.0, altitude=0.0), Position3D(longitude=1.0, latitude=1.0, altitude=1.0)])]) # Now GeometryCollection( type="GeometryCollection", geometries=[ Point(type="Point", coordinates=[0, 0]), # 2D point LineString( type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] # 3D LineString ), ], ) >>> ValidationError: 1 validation error for GeometryCollection geometries Value error, GeometryCollection cannot have mixed Z dimensionality. [type=value_error, input_value=[Point(bbox=None, type='P...de=1.0, altitude=1.0)])], input_type=list] For further information visit https://errors.pydantic.dev/2.11/v/value_error ``` ## [1.2.0] - 2024-12-19 * drop python 3.8 support * add python 3.13 support ## [1.1.2] - 2024-10-22 * relax `bbox` validation and allow antimeridian crossing bboxes ## [1.1.1] - 2024-08-29 * add python 3.12 support * switch to `flit-core` for packaging build backend ## [1.1.0] - 2024-05-10 ### Added * Add Position2D and Position3D of type NamedTuple (author @impocode, https://github.com/developmentseed/geojson-pydantic/pull/161) ## [1.0.2] - 2024-01-16 ### Fixed * Temporary workaround for surfacing model attributes in FastAPI application (author @markus-work, https://github.com/developmentseed/geojson-pydantic/pull/153) ## [1.0.1] - 2023-10-04 ### Fixed * Model serialization when using include/exclude (ref: https://github.com/developmentseed/geojson-pydantic/pull/148) ## [1.0.0] - 2023-07-24 ### Fixed * reduce validation error message verbosity when discriminating Geometry types * MultiPoint WKT now includes parentheses around each Point ### Added * more tests for `GeometryCollection` warnings ### Changed * update pydantic requirement to `~=2.0` * update pydantic `FeatureCollection` generic model to allow named features in the generated schemas. ```python # before FeatureCollection[Geometry, Properties] # now FeatureCollection[Feature[Geometry, Properties]] ``` * raise `ValueError` in `geometries.parse_geometry_obj` instead of `ValidationError` ```python # before parse_geometry_obj({"type": "This type", "obviously": "doesn't exist"}) >> ValidationError # now parse_geometry_obj({"type": "This type", "obviously": "doesn't exist"}) >> ValueError("Unknown type: This type") ``` * update JSON serializer to exclude null `bbox` and `id` ```python # before Point(type="Point", coordinates=[0, 0]).json() >> '{"type":"Point","coordinates":[0.0,0.0],"bbox":null}' # now Point(type="Point", coordinates=[0, 0]).model_dump_json() >> '{"type":"Point","coordinates":[0.0,0.0]}' ``` * delete `geojson_pydantic.geo_interface.GeoInterfaceMixin` and replaced by `geojson_pydantic.base._GeoJsonBase` class * delete `geojson_pydantic.types.validate_bbox` ## [0.6.3] - 2023-07-02 * limit pydantic requirement to `~=1.0` ## [0.6.2] - 2023-05-16 ### Added * Additional bbox validation (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/122) ## [0.6.1] - 2023-05-12 ### Fixed * Fix issue with null bbox validation (author @bmschmidt, https://github.com/developmentseed/geojson-pydantic/pull/119) ## [0.6.0] - 2023-05-09 No change since 0.6.0a0 ## [0.6.0a0] - 2023-04-04 ### Added - Validate order of bounding box values. (author @moradology, https://github.com/developmentseed/geojson-pydantic/pull/114) - Enforce required keys and avoid defaults. This aim to follow the geojson specification to the letter. ```python # Before Feature(geometry=Point(coordinates=(0,0))) # Now Feature( type="Feature", geometry=Point( type="Point", coordinates=(0,0) ), properties=None, ) ``` - Add has_z function to Geometries (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/103) - Add optional bbox to geometries. (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/108) - Add support for nested GeometryCollection and a corresponding warning. (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/111) ### Changed - Refactor and simplify WKT construction (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/97) - Support empty geometry coordinates (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/100) - Refactored `__geo_interface__` to be a Mixin which returns `self.dict` (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/105) - GeometryCollection containing a single geometry or geometries of only one type will now produce a warning. (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/111) ### Fixed - Do not validates arbitrary dictionaries. Make `Type` a mandatory key for objects (author @vincentsarago, https://github.com/developmentseed/geojson-pydantic/pull/94) - Add Geometry discriminator when parsing geometry objects (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/101) - Mixed Dimensionality WKTs (make sure the coordinates are either all 2D or 3D) (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/107) - Allow Feature's **id** to be either a String or a Number (author @vincentsarago, https://github.com/developmentseed/geojson-pydantic/pull/91) ### Removed - Python 3.7 support (author @vincentsarago, https://github.com/developmentseed/geojson-pydantic/pull/94) - Unused `LinearRing` Model (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/106) ## [0.5.0] - 2022-12-16 ### Added - python 3.11 support ### Fixed - Derive WKT type from Geometry's type instead of class name (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/81) ### Changed - Replace `NumType` with `float` throughout (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/83) - `__geo_interface__` definition to not use pydantic `BaseModel.dict()` method and better match the specification - Adjusted mypy configuration and updated type definitions to satisfy all rules (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/87) - Updated pre-commit config to run mypy on the whole library instead of individual changed files. - Defaults are more explicit. This keeps pyright from thinking they are required. ### Removed - Remove `validate` classmethods used to implicitly load json strings (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/88) ## [0.4.3] - 2022-07-18 ### Fixed - The bbox key should not be in a `__geo_interface__` object if the bbox is None (author @yellowcap, https://github.com/developmentseed/geojson-pydantic/pull/77) ## [0.4.2] - 2022-06-13 ### Added - `GeometryCollection` as optional input to geometry field in `Feature` (author @davidraleigh, https://github.com/developmentseed/geojson-pydantic/pull/72) ## [0.4.1] - 2022-06-10 ### Added - `Geometry` and `GeometryCollection` validation from dict or string (author @Vikka, https://github.com/developmentseed/geojson-pydantic/pull/69) ```python Point.validate('{"coordinates": [1.0, 2.0], "type": "Point"}') >> Point(coordinates=(1.0, 2.0), type='Point') ``` - `Feature` and `FeatureCollection` validation from dict or string ```python FeatureCollection.validate('{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"coordinates": [1.0, 2.0], "type": "Point"}}]}') >> FeatureCollection(type='FeatureCollection', features=[Feature(type='Feature', geometry=Point(coordinates=(1.0, 2.0), type='Point'), properties=None, id=None, bbox=None)], bbox=None) ``` ## [0.4.0] - 2022-06-03 ### Added - `.wkt` property for Geometry object ```python from geojson_pydantic.geometries import Point Point(coordinates=(1, 2)).wkt >> 'POINT (1.0 2.0)' ``` - `.exterior` and `.interiors` properties for `geojson_pydantic.geometries.Polygon` object. ```python from geojson_pydantic.geometries import Polygon polygon = Polygon( coordinates=[ [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], [(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)], ] ) polygon.exterior >> [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)] list(polygon.interiors) >> [[(2.0, 2.0), (2.0, 4.0), (4.0, 4.0), (4.0, 2.0), (2.0, 2.0)]] ``` - `__geo_interface__` to `geojson_pydantic.geometries.GeometryCollection` object - `__geo_interface__` to `geojson_pydantic.feature.Feature` and `geojson_pydantic.feature.FeatureCollection` object - `geojson_pydantic.__all__` to declaring public objects (author @farridav, https://github.com/developmentseed/geojson-pydantic/pull/52) ### Changed - switch to `pyproject.toml` - rename `geojson_pydantic.version` to `geojson_pydantic.__version__` ### Fixed - changelog compare links ## [0.3.4] - 2022-04-28 - Fix optional geometry and bbox fields on `Feature`; allowing users to pass in `None` or even omit either field (author @moradology, https://github.com/developmentseed/geojson-pydantic/pull/56) - Fix `Polygon.from_bounds` to respect geojson specification and return counterclockwise linear ring (author @jmfee-usgs, https://github.com/developmentseed/geojson-pydantic/pull/49) ## [0.3.3] - 2022-03-04 - Follow geojson specification and make feature geometry optional (author @yellowcap, https://github.com/developmentseed/geojson-pydantic/pull/47) ```python from geojson_pydantic import Feature # Before feature = Feature(type="Feature", geometry=None, properties={}) >> ValidationError: 1 validation error for Feature geometry none is not an allowed value (type=type_error.none.not_allowed) # Now feature = Feature(type="Feature", geometry=None, properties={}) assert feature.geometry is None ``` ## [0.3.2] - 2022-02-21 - fix `parse_geometry_obj` potential bug (author @geospatial-jeff, https://github.com/developmentseed/geojson-pydantic/pull/45) - improve type definition (and validation) for geometry coordinate arrays (author @geospatial-jeff, https://github.com/developmentseed/geojson-pydantic/pull/44) ## [0.3.1] - 2021-08-04 ### Added - `Polygon.from_bounds` class method to create a Polygon geometry from a bounding box. ```python from geojson_pydantic import Polygon print(Polygon.from_bounds((1, 2, 3, 4)).dict(exclude_none=True)) >> {'coordinates': [[(1.0, 2.0), (1.0, 4.0), (3.0, 4.0), (3.0, 2.0), (1.0, 2.0)]], 'type': 'Polygon'} ``` ### Fixed - Added validation for Polygons with zero size. ## [0.3.0] - 2021-05-25 ### Added - `Feature` and `FeatureCollection` model generics to support custom geometry and/or properties validation (author @iwpnd, https://github.com/developmentseed/geojson-pydantic/pull/29) ```python from pydantic import BaseModel from geojson_pydantic.features import Feature from geojson_pydantic.geometries import Polygon class MyFeatureProperties(BaseModel): name: str value: int feature = Feature[Polygon, MyFeatureProperties]( **{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [13.38272,52.46385], [13.42786,52.46385], [13.42786,52.48445], [13.38272,52.48445], [13.38272,52.46385] ] ] }, "properties": { "name": "test", "value": 1 } } ) ``` - Top level export (https://github.com/developmentseed/geojson-pydantic/pull/34) ```python # before from geojson_pydantic.features import Feature, FeatureCollection from geojson_pydantic.geometries import Polygon # now from geojson_pydantic import Feature, Polygon ``` ### Removed - Drop python 3.6 support - Renamed `utils.py` to `types.py` - Removed `Coordinate` type in `geojson_pydantic.features` (replaced by `Position`) ## [0.2.3] - 2021-05-05 ### Fixed - incorrect version number set in `__init__.py` ## [0.2.2] - 2020-12-29 ### Added - Made collections iterable (#12) - Added `parse_geometry_obj` function (#9) ## [0.2.1] - 2020-08-07 Although the type file was added in `0.2.0` it wasn't included in the distributed package. Use this version `0.2.1` for type annotations. ### Fixed - Correct package type information files ## [0.2.0] - 2020-08-06 ### Added - Added documentation on locally running tests (#3) - Publish type information (#6) ### Changed - Removed geojson dependency (#4) ### Fixed - Include MultiPoint as a valid geometry for a Feature (#1) ## [0.1.0] - 2020-05-21 ### Added - Initial Release [unreleased]: https://github.com/developmentseed/geojson-pydantic/compare/2.0.0...HEAD [2.0.0]: https://github.com/developmentseed/geojson-pydantic/compare/1.2.0...2.0.0 [1.2.0]: https://github.com/developmentseed/geojson-pydantic/compare/1.1.2...1.2.0 [1.1.2]: https://github.com/developmentseed/geojson-pydantic/compare/1.1.1...1.1.2 [1.1.1]: https://github.com/developmentseed/geojson-pydantic/compare/1.1.0...1.1.1 [1.1.0]: https://github.com/developmentseed/geojson-pydantic/compare/1.0.2...1.1.0 [1.0.2]: https://github.com/developmentseed/geojson-pydantic/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/developmentseed/geojson-pydantic/compare/1.0.0...1.0.1 [1.0.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.3...1.0.0 [0.6.3]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.2...0.6.3 [0.6.2]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.1...0.6.2 [0.6.1]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.0...0.6.1 [0.6.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.0a0...0.6.0 [0.6.0a]: https://github.com/developmentseed/geojson-pydantic/compare/0.5.0...0.6.0a0 [0.5.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.4.3...0.5.0 [0.4.3]: https://github.com/developmentseed/geojson-pydantic/compare/0.4.2...0.4.3 [0.4.2]: https://github.com/developmentseed/geojson-pydantic/compare/0.4.1...0.4.2 [0.4.1]: https://github.com/developmentseed/geojson-pydantic/compare/0.4.0...0.4.1 [0.4.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.4...0.4.0 [0.3.4]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.3...0.3.4 [0.3.3]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.2...0.3.3 [0.3.2]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.1...0.3.2 [0.3.1]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.0...0.3.1 [0.3.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.2.3...0.3.0 [0.2.3]: https://github.com/developmentseed/geojson-pydantic/compare/0.2.2...0.2.3 [0.2.2]: https://github.com/developmentseed/geojson-pydantic/compare/0.2.1...0.2.2 [0.2.1]: https://github.com/developmentseed/geojson-pydantic/compare/0.2.0...0.2.1 [0.2.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.1.0...0.2.0 [0.1.0]: https://github.com/developmentseed/geojson-pydantic/compare/005f3e57ad07272c99c54302decc63eec12175c9...0.1.0 geojson-pydantic-2.0.0/CONTRIBUTING.md000066400000000000000000000015371500622330400172200ustar00rootroot00000000000000# Contributing To run the tests, first install the package in a virtual environment: ```sh virtualenv venv source venv/bin/activate python -m pip install -e '.[test]' ``` You can then run the tests with the following command: ```sh python -m pytest --cov geojson_pydantic --cov-report term-missing ``` This repo is set to use pre-commit to run `isort`, `flake8`, `pydocstring`, `black` ("uncompromising Python code formatter") and `mypy` when committing new code. ``` sh pre-commit install ``` ## Release we use https://github.com/c4urself/bump2version to update the package version. ``` # Install bump2version $ pip install --upgrade bump2version # Update version (edit files, commit and create tag) # this will do `0.2.1 -> 0.2.2` because we use the `patch` tag $ bump2version patch # Push change and tag to github $ git push origin main --tags ``` geojson-pydantic-2.0.0/LICENSE000066400000000000000000000020611500622330400157650ustar00rootroot00000000000000MIT License Copyright (c) 2020 Development Seed Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. geojson-pydantic-2.0.0/README.md000066400000000000000000000055141500622330400162450ustar00rootroot00000000000000# geojson-pydantic

Pydantic models for GeoJSON.

Test Coverage Package version Downloads License Conda

--- **Documentation**: https://developmentseed.org/geojson-pydantic/ **Source Code**: https://github.com/developmentseed/geojson-pydantic --- ## Description `geojson_pydantic` provides a suite of Pydantic models matching the [GeoJSON specification rfc7946](https://datatracker.ietf.org/doc/html/rfc7946). Those models can be used for creating or validating geojson data. ## Install ```bash $ python -m pip install -U pip $ python -m pip install geojson-pydantic ``` Or install from source: ```bash $ python -m pip install -U pip $ python -m pip install git+https://github.com/developmentseed/geojson-pydantic.git ``` Install with conda from [`conda-forge`](https://anaconda.org/conda-forge/geojson-pydantic): ```bash $ conda install -c conda-forge geojson-pydantic ``` ## Contributing See [CONTRIBUTING.md](https://github.com/developmentseed/geojson-pydantic/blob/main/CONTRIBUTING.md). ## Changes See [CHANGES.md](https://github.com/developmentseed/geojson-pydantic/blob/main/CHANGELOG.md). ## Authors Initial implementation by @geospatial-jeff; taken liberally from https://github.com/arturo-ai/stac-pydantic/ See [contributors](hhttps://github.com/developmentseed/geojson-pydantic/graphs/contributors) for a listing of individual contributors. ## License See [LICENSE](https://github.com/developmentseed/geojson-pydantic/blob/main/LICENSE) geojson-pydantic-2.0.0/docs/000077500000000000000000000000001500622330400157115ustar00rootroot00000000000000geojson-pydantic-2.0.0/docs/mkdocs.yml000066400000000000000000000034001500622330400177110ustar00rootroot00000000000000# Project Information site_name: 'geojson-pydantic' site_description: 'Pydantic models for GeoJSON' docs_dir: 'src' site_dir: 'build' # Repository repo_name: 'developmentseed/geojson-pydantic' repo_url: 'https://github.com/developmentseed/geojson-pydantic' edit_uri: 'blob/main/src/' site_url: 'https://developmentseed.org/geojson-pydantic/' # Social links extra: social: - icon: 'fontawesome/brands/github' link: 'https://github.com/developmentseed' - icon: 'fontawesome/brands/twitter' link: 'https://twitter.com/developmentseed' # Layout nav: - Home: 'index.md' - Intro: 'intro.md' - Migration guides: - v0.6 -> v1.0: migrations/v1.0_migration.md - Development - Contributing: 'contributing.md' - Release: 'release-notes.md' # Theme theme: icon: logo: 'material/home' repo: 'fontawesome/brands/github' name: 'material' language: 'en' palette: primary: 'pink' accent: 'light pink' font: text: 'Nunito Sans' code: 'Fira Code' plugins: - search # These extensions are chosen to be a superset of Pandoc's Markdown. # This way, I can write in Pandoc's Markdown and have it be supported here. # https://pandoc.org/MANUAL.html markdown_extensions: - admonition - attr_list - codehilite: guess_lang: false - def_list - footnotes - pymdownx.arithmatex - pymdownx.betterem - pymdownx.caret: insert: false - pymdownx.details - pymdownx.emoji - pymdownx.escapeall: hardbreak: true nbsp: true - pymdownx.magiclink: hide_protocol: true repo_url_shortener: true - pymdownx.smartsymbols - pymdownx.superfences - pymdownx.tasklist: custom_checkbox: true - pymdownx.tilde - toc: permalink: true geojson-pydantic-2.0.0/docs/src/000077500000000000000000000000001500622330400165005ustar00rootroot00000000000000geojson-pydantic-2.0.0/docs/src/contributing.md000077700000000000000000000000001500622330400244032../../CONTRIBUTING.mdustar00rootroot00000000000000geojson-pydantic-2.0.0/docs/src/index.md000077700000000000000000000000001500622330400220312../../README.mdustar00rootroot00000000000000geojson-pydantic-2.0.0/docs/src/intro.md000066400000000000000000000140311500622330400201540ustar00rootroot00000000000000 ## Usage ```python from geojson_pydantic import Feature, FeatureCollection, Point geojson_feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.38272, 52.46385], }, "properties": { "name": "jeff", }, } feat = Feature(**geojson_feature) assert feat.type == "Feature" assert type(feat.geometry) == Point assert feat.properties["name"] == "jeff" fc = FeatureCollection(type="FeatureCollection", features=[geojson_feature, geojson_feature]) assert fc.type == "FeatureCollection" assert len(fc.features) == 2 assert type(fc.features[0].geometry) == Point assert fc.features[0].properties["name"] == "jeff" ``` ## Geometry Model methods and properties - `__geo_interface__`: GeoJSON-like protocol for geo-spatial (GIS) vector data ([spec](https://gist.github.com/sgillies/2217756#__geo_interface)). - `has_z`: returns true if any coordinate has a Z value. - `wkt`: returns the Well Known Text representation of the geometry. ##### For Polygon geometry - `exterior`: returns the exterior Linear Ring of the polygon. - `interiors`: returns the interiors (Holes) of the polygon. - `Polygon.from_bounds(xmin, ymin, xmax, ymax)`: creates a Polygon geometry from a bounding box. ##### For GeometryCollection and FeatureCollection - `iter()`: iterates over geometries or features - `length`: returns geometries or features count ## Advanced usage In `geojson_pydantic` we've implemented pydantic's [Generic Models](https://pydantic-docs.helpmanual.io/usage/models/#generic-models) which allow the creation of more advanced models to validate either the geometry type or the properties. In order to make use of this generic typing, there are two steps: first create a new model, then use that model to validate your data. To create a model using a `Generic` type, you **HAVE TO** pass `Type definitions` to the `Feature` model in form of `Feature[Geometry Type, Properties Type]`. Then pass your data to this constructor. By default `Feature` and `FeatureCollections` are defined using `geojson_pydantic.geometries.Geometry` for the geometry and `typing.Dict` for the properties. Here's an example where we want to validate that GeoJSON features have Polygon types, but don't do any specific property validation. ```python from typing import Dict from geojson_pydantic import Feature, Polygon from pydantic import BaseModel geojson_feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.38272, 52.46385], }, "properties": { "name": "jeff", }, } # Define a Feature model with Geometry as `Polygon` and Properties as `Dict` MyPolygonFeatureModel = Feature[Polygon, Dict] feat = MyPolygonFeatureModel(**geojson_feature) # should raise Validation Error because `geojson_feature` is a point >>> ValidationError: 3 validation errors for Feature[Polygon, Dict] ... geometry -> type unexpected value; permitted: 'Polygon' (type=value_error.const; given=Point; permitted=['Polygon']) geojson_feature = { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [13.38272, 52.46385], [13.42786, 52.46385], [13.42786, 52.48445], [13.38272, 52.48445], [13.38272, 52.46385], ] ], }, "properties": { "name": "jeff", }, } feat = MyPolygonFeatureModel(**geojson_feature) assert type(feat.geometry) == Polygon ``` Or with optional geometry ```python from geojson_pydantic import Feature, Point from typing import Optional MyPointFeatureModel = Feature[Optional[Point], Dict] assert MyPointFeatureModel(type="Feature", geometry=None, properties={}).geometry is None assert MyPointFeatureModel(type="Feature", geometry=Point(type="Point", coordinates=(0,0)), properties={}).geometry is not None ``` And now with constrained properties ```python from typing_extensions import Annotated from geojson_pydantic import Feature, Point from pydantic import BaseModel # Define a Feature model with Geometry as `Point` and Properties as a constrained Model class MyProps(BaseModel): name: Annotated[str, Field(pattern=r"^(drew|vincent)$")] MyPointFeatureModel = Feature[Point, MyProps] geojson_feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.38272, 52.46385], }, "properties": { "name": "jeff", }, } feat = MyPointFeatureModel(**geojson_feature) >>> ValidationError: 1 validation error for Feature[Point, MyProps] properties -> name string does not match regex "^(drew|vincent)$" (type=value_error.str.regex; pattern=^(drew|vincent)$) geojson_feature["properties"]["name"] = "drew" feat = MyPointFeatureModel(**geojson_feature) assert feat.properties.name == "drew" ``` ## Enforced Keys Starting with version `0.6.0`, geojson-pydantic's classes will not define default keys such has `type`, `geometry` or `properties`. This is to make sure the library does well its first goal, which is `validating` GeoJSON object based on the [specification](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.1) o A GeoJSON object has a member with the name "type". The value of the member MUST be one of the GeoJSON types. o A Feature object HAS a "type" member with the value "Feature". o A Feature object HAS a member with the name "geometry". The value of the geometry member SHALL be either a Geometry object as defined above or, in the case that the Feature is unlocated, a JSON null value. o A Feature object HAS a member with the name "properties". The value of the properties member is an object (any JSON object or a JSON null value). ```python from geojson_pydantic import Point ## Before 0.6 Point(coordinates=(0,0)) >> Point(type='Point', coordinates=(0.0, 0.0), bbox=None) ## After 0.6 Point(coordinates=(0,0)) >> ValidationError: 1 validation error for Point type field required (type=value_error.missing) Point(type="Point", coordinates=(0,0)) >> Point(type='Point', coordinates=(0.0, 0.0), bbox=None) ``` geojson-pydantic-2.0.0/docs/src/migrations/000077500000000000000000000000001500622330400206545ustar00rootroot00000000000000geojson-pydantic-2.0.0/docs/src/migrations/v1.0_migration.md000066400000000000000000000051471500622330400237420ustar00rootroot00000000000000`geojson-pydantic` version 1.0 introduced [many breaking changes](../release-notes.md). This document aims to help with migrating your code to use `geojson-pydantic` 1.0. ## Pydantic 2.0 The biggest update introduced in **1.0** is the new pydantic *major* version requirement [**~2.0**](https://docs.pydantic.dev/2.0/migration/). In addition to being faster, this new major version has plenty of new API which we used in `geojson-pydantic` (like the new `model_serializer` method). ```python from geojson_pydantic import Point # Before Point.dict() # serialize to dict object Point.json() # serialize to json string with open("point.geojson") as f: Point.parse_file(f) # parse file content to model p = {} Point.parse_obj(obj) # parse dict object ################## # Now (geojson-pydantic ~= 1.0) Point.model_dump() Point.model_dump_json() with open("point.geojson") as f: Point.model_validate_json(f.read()) p = {} Point.model_validate(obj) ``` ref: https://github.com/developmentseed/geojson-pydantic/pull/130 ## FeatureCollection Generic model In **1.0** we updated the generic FeatureCollection model to depends only on a generic Feature model. ```python # Before FeatureCollection[Geometry, Properties] # Now (geojson-pydantic ~= 1.0) FeatureCollection[Feature[Geometry, Properties]] ``` e.g ```python from pydantic import BaseModel from geojson_pydantic import Feature, FeatureCollection, Polygon class CustomProperties(BaseModel): id: str description: str size: int # Create a new FeatureCollection Model which should only # Accept Features with Polygon geometry type and matching the properties MyFc = FeatureCollection[Feature[Polygon, CustomProperties]] ``` ref: https://github.com/developmentseed/geojson-pydantic/issues/134 ## Exclude `bbox` and `id` if null Using the new pydantic `model_serializer` method, we are now able to `customize` JSON output for the models to better match the GeoJSON spec ```python # Before Point(type="Point", coordinates=[0, 0]).json() >> '{"type":"Point","coordinates":[0.0,0.0],"bbox":null}' # Now (geojson-pydantic ~= 1.0) Point(type="Point", coordinates=[0, 0]).model_dump_json() >> '{"type":"Point","coordinates":[0.0,0.0]}' ``` ref: https://github.com/developmentseed/geojson-pydantic/issues/125 ## Change in WKT output for Multi* geometries ```python from geojson_pydantic import MultiPoint geom = MultiPoint(type='MultiPoint', coordinates=[(1.0, 2.0, 3.0)]) # Before print(geom.wkt) >> MULTIPOINT Z (1 2 3) # Now (geojson-pydantic ~= 1.0) print(geom.wkt) >> MULTIPOINT Z ((1 2 3)) ``` ref: https://github.com/developmentseed/geojson-pydantic/issues/139 geojson-pydantic-2.0.0/docs/src/release-notes.md000077700000000000000000000000001500622330400240222../../CHANGELOG.mdustar00rootroot00000000000000geojson-pydantic-2.0.0/geojson_pydantic/000077500000000000000000000000001500622330400203205ustar00rootroot00000000000000geojson-pydantic-2.0.0/geojson_pydantic/__init__.py000066400000000000000000000007001500622330400224260ustar00rootroot00000000000000"""geojson-pydantic.""" from .features import Feature, FeatureCollection # noqa from .geometries import ( # noqa GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) __version__ = "2.0.0" __all__ = [ "Feature", "FeatureCollection", "GeometryCollection", "LineString", "MultiLineString", "MultiPoint", "MultiPolygon", "Point", "Polygon", ] geojson-pydantic-2.0.0/geojson_pydantic/base.py000066400000000000000000000057661500622330400216220ustar00rootroot00000000000000"""pydantic BaseModel for GeoJSON objects.""" from __future__ import annotations import warnings from typing import Any, Dict, List, Optional, Set from pydantic import BaseModel, SerializationInfo, field_validator, model_serializer from geojson_pydantic.types import BBox class _GeoJsonBase(BaseModel): bbox: Optional[BBox] = None # These fields will not be included when serializing in json mode # `.model_dump_json()` or `.model_dump(mode="json")` __geojson_exclude_if_none__: Set[str] = {"bbox"} @property def __geo_interface__(self) -> Dict[str, Any]: """GeoJSON-like protocol for geo-spatial (GIS) vector data. ref: https://gist.github.com/sgillies/2217756#__geo_interface """ return self.model_dump(mode="json") @field_validator("bbox") def validate_bbox(cls, bbox: Optional[BBox]) -> Optional[BBox]: """Validate BBox values are ordered correctly.""" # If bbox is None, there is nothing to validate. if bbox is None: return None # A list to store any errors found so we can raise them all at once. errors: List[str] = [] # Determine where the second position starts. 2 for 2D, 3 for 3D. offset = len(bbox) // 2 # Check X if bbox[0] > bbox[offset]: warnings.warn( f"BBOX crossing the Antimeridian line, Min X ({bbox[0]}) > Max X ({bbox[offset]}).", UserWarning, stacklevel=1, ) # Check Y if bbox[1] > bbox[1 + offset]: errors.append(f"Min Y ({bbox[1]}) must be <= Max Y ({bbox[1 + offset]}).") # If 3D, check Z values. if offset > 2 and bbox[2] > bbox[2 + offset]: errors.append(f"Min Z ({bbox[2]}) must be <= Max Z ({bbox[2 + offset]}).") # Raise any errors found. if errors: raise ValueError("Invalid BBox. Error(s): " + " ".join(errors)) return bbox # This return is untyped due to a workaround until this issue is resolved: # https://github.com/tiangolo/fastapi/discussions/10661 @model_serializer(when_used="always", mode="wrap") def clean_model(self, serializer: Any, info: SerializationInfo): # type: ignore [no-untyped-def] """Custom Model serializer to match the GeoJSON specification. Used to remove fields which are optional but cannot be null values. """ # This seems like the best way to have the least amount of unexpected consequences. # We want to avoid forcing values in `exclude_none` or `exclude_unset` which could # cause issues or unexpected behavior for downstream users. # ref: https://github.com/pydantic/pydantic/issues/6575 data: Dict[str, Any] = serializer(self) # Only remove fields when in JSON mode. if info.mode_is_json(): for field in self.__geojson_exclude_if_none__: if field in data and data[field] is None: del data[field] return data geojson-pydantic-2.0.0/geojson_pydantic/features.py000066400000000000000000000026131500622330400225120ustar00rootroot00000000000000"""pydantic models for GeoJSON Feature objects.""" from typing import Any, Dict, Generic, Iterator, List, Literal, Optional, TypeVar, Union from pydantic import BaseModel, Field, StrictInt, StrictStr, field_validator from geojson_pydantic.base import _GeoJsonBase from geojson_pydantic.geometries import Geometry Props = TypeVar("Props", bound=Union[Dict[str, Any], BaseModel]) Geom = TypeVar("Geom", bound=Geometry) class Feature(_GeoJsonBase, Generic[Geom, Props]): """Feature Model""" type: Literal["Feature"] geometry: Union[Geom, None] = Field(...) properties: Union[Props, None] = Field(...) id: Optional[Union[StrictInt, StrictStr]] = None __geojson_exclude_if_none__ = {"bbox", "id"} @field_validator("geometry", mode="before") def set_geometry(cls, geometry: Any) -> Any: """set geometry from geo interface or input""" if hasattr(geometry, "__geo_interface__"): return geometry.__geo_interface__ return geometry Feat = TypeVar("Feat", bound=Feature) class FeatureCollection(_GeoJsonBase, Generic[Feat]): """FeatureCollection Model""" type: Literal["FeatureCollection"] features: List[Feat] def iter(self) -> Iterator[Feat]: """iterate over features""" return iter(self.features) @property def length(self) -> int: """return features length""" return len(self.features) geojson-pydantic-2.0.0/geojson_pydantic/geometries.py000066400000000000000000000252351500622330400230440ustar00rootroot00000000000000"""pydantic models for GeoJSON Geometry objects.""" from __future__ import annotations import abc import warnings from typing import Any, Iterator, List, Literal, Union from pydantic import Field, field_validator from typing_extensions import Annotated from geojson_pydantic.base import _GeoJsonBase from geojson_pydantic.types import ( LinearRing, LineStringCoords, MultiLineStringCoords, MultiPointCoords, MultiPolygonCoords, PolygonCoords, Position, ) def _position_wkt_coordinates(coordinates: Position, force_z: bool = False) -> str: """Converts a Position to WKT Coordinates.""" wkt_coordinates = " ".join(str(number) for number in coordinates) if force_z and len(coordinates) < 3: wkt_coordinates += " 0.0" return wkt_coordinates def _position_has_z(position: Position) -> bool: return len(position) == 3 def _position_list_wkt_coordinates( coordinates: List[Position], force_z: bool = False ) -> str: """Converts a list of Positions to WKT Coordinates.""" return ", ".join( _position_wkt_coordinates(position, force_z) for position in coordinates ) def _position_list_has_z(positions: List[Position]) -> bool: """Checks if any position in a list has a Z.""" return any(_position_has_z(position) for position in positions) def _lines_wtk_coordinates( coordinates: List[LineStringCoords], force_z: bool = False ) -> str: """Converts lines to WKT Coordinates.""" return ", ".join( f"({_position_list_wkt_coordinates(line, force_z)})" for line in coordinates ) def _lines_has_z(lines: List[LineStringCoords]) -> bool: """Checks if any position in a list has a Z.""" return any( _position_has_z(position) for positions in lines for position in positions ) def _polygons_wkt_coordinates( coordinates: List[PolygonCoords], force_z: bool = False ) -> str: return ", ".join( f"({_lines_wtk_coordinates(polygon, force_z)})" for polygon in coordinates ) class _GeometryBase(_GeoJsonBase, abc.ABC): """Base class for geometry models""" type: str coordinates: Any @abc.abstractmethod def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" ... @property @abc.abstractmethod def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" ... @property def wkt(self) -> str: """Return the Well Known Text representation.""" # Start with the WKT Type wkt = self.type.upper() has_z = self.has_z if self.coordinates: # If any of the coordinates have a Z add a "Z" to the WKT wkt += " Z " if has_z else " " # Add the rest of the WKT inside parentheses wkt += f"({self.__wkt_coordinates__(self.coordinates, force_z=has_z)})" else: # Otherwise it will be "EMPTY" wkt += " EMPTY" return wkt class Point(_GeometryBase): """Point Model""" type: Literal["Point"] coordinates: Position def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _position_wkt_coordinates(coordinates, force_z) @property def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _position_has_z(self.coordinates) class MultiPoint(_GeometryBase): """MultiPoint Model""" type: Literal["MultiPoint"] coordinates: MultiPointCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return ", ".join( f"({_position_wkt_coordinates(position, force_z)})" for position in coordinates ) @property def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _position_list_has_z(self.coordinates) class LineString(_GeometryBase): """LineString Model""" type: Literal["LineString"] coordinates: LineStringCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _position_list_wkt_coordinates(coordinates, force_z) @property def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _position_list_has_z(self.coordinates) class MultiLineString(_GeometryBase): """MultiLineString Model""" type: Literal["MultiLineString"] coordinates: MultiLineStringCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _lines_wtk_coordinates(coordinates, force_z) @property def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _lines_has_z(self.coordinates) class Polygon(_GeometryBase): """Polygon Model""" type: Literal["Polygon"] coordinates: PolygonCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _lines_wtk_coordinates(coordinates, force_z) @field_validator("coordinates") def check_closure(cls, coordinates: List) -> List: """Validate that Polygon is closed (first and last coordinate are the same).""" if any(ring[-1] != ring[0] for ring in coordinates): raise ValueError("All linear rings have the same start and end coordinates") return coordinates @property def exterior(self) -> Union[LinearRing, None]: """Return the exterior Linear Ring of the polygon.""" return self.coordinates[0] if self.coordinates else None @property def interiors(self) -> Iterator[LinearRing]: """Interiors (Holes) of the polygon.""" yield from ( interior for interior in self.coordinates[1:] if len(self.coordinates) > 1 ) @property def has_z(self) -> bool: """Checks if any coordinates have a Z value.""" return _lines_has_z(self.coordinates) @classmethod def from_bounds( cls, xmin: float, ymin: float, xmax: float, ymax: float ) -> "Polygon": """Create a Polygon geometry from a boundingbox.""" return cls( type="Polygon", coordinates=[ [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax), (xmin, ymin)] ], ) class MultiPolygon(_GeometryBase): """MultiPolygon Model""" type: Literal["MultiPolygon"] coordinates: MultiPolygonCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _polygons_wkt_coordinates(coordinates, force_z) @property def has_z(self) -> bool: """Checks if any coordinates have a Z value.""" return any(_lines_has_z(polygon) for polygon in self.coordinates) @field_validator("coordinates") def check_closure(cls, coordinates: List) -> List: """Validate that Polygon is closed (first and last coordinate are the same).""" if any(ring[-1] != ring[0] for polygon in coordinates for ring in polygon): raise ValueError("All linear rings have the same start and end coordinates") return coordinates class GeometryCollection(_GeoJsonBase): """GeometryCollection Model""" type: Literal["GeometryCollection"] geometries: List[Geometry] def iter(self) -> Iterator[Geometry]: """iterate over geometries""" return iter(self.geometries) @property def length(self) -> int: """return geometries length""" return len(self.geometries) @property def wkt(self) -> str: """Return the Well Known Text representation.""" # Each geometry will check its own coordinates for Z and include "Z" in the wkt # if necessary. Rather than looking at the coordinates for each of the geometries # again, we can just get the wkt from each of them and check if there is a Z # anywhere in the text. # Get the wkt from each of the geometries in the collection geometries = ( f'({", ".join(geom.wkt for geom in self.geometries)})' if self.geometries else "EMPTY" ) # If any of them contain `Z` add Z to the output wkt z = " Z " if "Z" in geometries else " " return f"{self.type.upper()}{z}{geometries}" @property def has_z(self) -> bool: """Checks if any coordinates have a Z value.""" return any(geom.has_z for geom in self.geometries) @field_validator("geometries") def check_geometries(cls, geometries: List) -> List: """Add warnings for conditions the spec does not explicitly forbid.""" if len(geometries) == 1: warnings.warn( "GeometryCollection should not be used for single geometries.", stacklevel=1, ) if any(geom.type == "GeometryCollection" for geom in geometries): warnings.warn( "GeometryCollection should not be used for nested GeometryCollections.", stacklevel=1, ) if len({geom.type for geom in geometries}) == 1: warnings.warn( "GeometryCollection should not be used for homogeneous collections.", stacklevel=1, ) if len({geom.has_z for geom in geometries}) == 2: raise ValueError("GeometryCollection cannot have mixed Z dimensionality.") return geometries Geometry = Annotated[ Union[ Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection, ], Field(discriminator="type"), ] GeometryCollection.model_rebuild() def parse_geometry_obj(obj: Any) -> Geometry: """ `obj` is an object that is supposed to represent a GeoJSON geometry. This method returns the reads the `"type"` field and returns the correct pydantic Geometry model. """ if "type" not in obj: raise ValueError("Missing 'type' field in geometry") if obj["type"] == "Point": return Point.model_validate(obj) elif obj["type"] == "MultiPoint": return MultiPoint.model_validate(obj) elif obj["type"] == "LineString": return LineString.model_validate(obj) elif obj["type"] == "MultiLineString": return MultiLineString.model_validate(obj) elif obj["type"] == "Polygon": return Polygon.model_validate(obj) elif obj["type"] == "MultiPolygon": return MultiPolygon.model_validate(obj) elif obj["type"] == "GeometryCollection": return GeometryCollection.model_validate(obj) raise ValueError(f"Unknown type: {obj['type']}") geojson-pydantic-2.0.0/geojson_pydantic/py.typed000066400000000000000000000000001500622330400220050ustar00rootroot00000000000000geojson-pydantic-2.0.0/geojson_pydantic/types.py000066400000000000000000000014731500622330400220430ustar00rootroot00000000000000"""Types for geojson_pydantic models""" from typing import List, NamedTuple, Tuple, Union from pydantic import Field from typing_extensions import Annotated BBox = Union[ Tuple[float, float, float, float], # 2D bbox Tuple[float, float, float, float, float, float], # 3D bbox ] Position2D = NamedTuple("Position2D", [("longitude", float), ("latitude", float)]) Position3D = NamedTuple( "Position3D", [("longitude", float), ("latitude", float), ("altitude", float)] ) Position = Union[Position2D, Position3D] # Coordinate arrays LineStringCoords = Annotated[List[Position], Field(min_length=2)] LinearRing = Annotated[List[Position], Field(min_length=4)] MultiPointCoords = List[Position] MultiLineStringCoords = List[LineStringCoords] PolygonCoords = List[LinearRing] MultiPolygonCoords = List[PolygonCoords] geojson-pydantic-2.0.0/pyproject.toml000066400000000000000000000051571500622330400177050ustar00rootroot00000000000000[project] name = "geojson-pydantic" description = "Pydantic data models for the GeoJSON spec." readme = "README.md" requires-python = ">=3.9" license = {file = "LICENSE"} authors = [ {name = "Drew Bollinger", email = "drew@developmentseed.org"}, ] keywords = ["geojson", "Pydantic"] classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: GIS", "Typing :: Typed", ] dynamic = ["version"] dependencies = ["pydantic~=2.0"] [project.optional-dependencies] test = ["pytest", "pytest-cov", "shapely"] dev = [ "pre-commit", "bump-my-version", ] docs = [ "mkdocs", "mkdocs-material", "pygments", ] [project.urls] Source = "https://github.com/developmentseed/geojson-pydantic" [build-system] requires = ["flit_core>=3.2,<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "geojson_pydantic" [tool.flit.sdist] exclude = [ "tests/", "docs/", ".github/", "CHANGELOG.md", "CONTRIBUTING.md", ] [tool.coverage.run] branch = true parallel = true [tool.coverage.report] exclude_lines = [ "no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] [tool.isort] profile = "black" known_first_party = ["geojson_pydantic"] known_third_party = ["pydantic"] default_section = "THIRDPARTY" [tool.mypy] plugins = ["pydantic.mypy"] disallow_untyped_calls = true disallow_untyped_defs = true disallow_incomplete_defs = true warn_redundant_casts = true warn_unused_ignores = true no_implicit_optional = true show_error_codes = true [tool.ruff.lint] select = [ "D1", # pydocstyle errors "E", # pycodestyle errors "W", # pycodestyle warnings "F", # flake8 "C", # flake8-comprehensions "B", # flake8-bugbear ] ignore = [ "E501", # line too long, handled by black "B008", # do not perform function calls in argument defaults "B905", # ignore zip() without an explicit strict= parameter, only support with python >3.10 ] [tool.ruff.lint.per-file-ignores] "tests/*.py" = ["D1"] [tool.bumpversion] current_version = "2.0.0" search = "{current_version}" replace = "{new_version}" regex = false tag = true commit = true tag_name = "{new_version}" [[tool.bumpversion.files]] filename = "geojson_pydantic/__init__.py" search = '__version__ = "{current_version}"' replace = '__version__ = "{new_version}"' geojson-pydantic-2.0.0/tests/000077500000000000000000000000001500622330400161235ustar00rootroot00000000000000geojson-pydantic-2.0.0/tests/test_base.py000066400000000000000000000060751500622330400204560ustar00rootroot00000000000000from typing import Set, Tuple, Union import pytest from pydantic import Field, ValidationError from geojson_pydantic.base import _GeoJsonBase BBOXES = ( (0, 100, 0, 0), (0, 0, 100, 0, 0, 0), (0, "a", 0, 0), # Invalid Type ) @pytest.mark.parametrize("values", BBOXES) def test_bbox_validation(values: Tuple) -> None: # Ensure validation is happening correctly on the base model with pytest.raises(ValidationError): _GeoJsonBase(bbox=values) def test_bbox_antimeridian() -> None: with pytest.warns(UserWarning): _GeoJsonBase(bbox=(100, 0, 0, 0)) @pytest.mark.parametrize("values", BBOXES) def test_bbox_validation_subclass(values: Tuple) -> None: # Ensure validation is happening correctly when subclassed class TestClass(_GeoJsonBase): test_field: str = None with pytest.raises(ValidationError): TestClass(bbox=values) @pytest.mark.parametrize("values", BBOXES) def test_bbox_validation_field(values: Tuple) -> None: # Ensure validation is happening correctly when used as a field class TestClass(_GeoJsonBase): geo: _GeoJsonBase with pytest.raises(ValidationError): TestClass(geo={"bbox": values}) def test_exclude_if_none() -> None: model = _GeoJsonBase() # Included in default dump assert model.model_dump() == {"bbox": None} # Not included when in json mode assert model.model_dump(mode="json") == {} # And not included in the output json string. assert model.model_dump_json() == "{}" # Included if it has a value model = _GeoJsonBase(bbox=(0, 0, 0, 0)) assert model.model_dump() == {"bbox": (0, 0, 0, 0)} assert model.model_dump(mode="json") == {"bbox": [0, 0, 0, 0]} assert model.model_dump_json() == '{"bbox":[0.0,0.0,0.0,0.0]}' # Since `validate_assignment` is not set, you can do this without an error. # The solution should handle this and not just look at if the field is set. model.bbox = None assert model.model_dump() == {"bbox": None} assert model.model_dump(mode="json") == {} assert model.model_dump_json() == "{}" def test_exclude_if_none_subclass() -> None: # Create a subclass that adds a field, and ensure it works. class TestClass(_GeoJsonBase): test_field: str = None __geojson_exclude_if_none__: Set[str] = {"bbox", "test_field"} assert TestClass().model_dump_json() == "{}" assert TestClass(test_field="a").model_dump_json() == '{"test_field":"a"}' assert ( TestClass(bbox=(0, 0, 0, 0)).model_dump_json() == '{"bbox":[0.0,0.0,0.0,0.0]}' ) def test_exclude_if_none_kwargs() -> None: # Create a subclass that adds fields and dumps it with kwargs to ensure # the kwargs are still being utilized. class TestClass(_GeoJsonBase): test_field: str = Field(default="test", alias="field") null_field: Union[str, None] = None model = TestClass(bbox=(0, 0, 0, 0)) assert ( model.model_dump_json(indent=2, by_alias=True, exclude_none=True) == """{ "bbox": [ 0.0, 0.0, 0.0, 0.0 ], "field": "test" }""" ) geojson-pydantic-2.0.0/tests/test_features.py000066400000000000000000000324751500622330400213650ustar00rootroot00000000000000import json from random import randint from typing import Any, Dict from uuid import uuid4 import pytest from pydantic import BaseModel, ValidationError from geojson_pydantic.features import Feature, FeatureCollection from geojson_pydantic.geometries import ( Geometry, GeometryCollection, MultiPolygon, Polygon, ) class GenericProperties(BaseModel): id: str description: str size: int properties: Dict[str, Any] = { "id": str(uuid4()), "description": str(uuid4()), "size": randint(0, 1000), } coordinates = [ [ [13.38272, 52.46385], [13.42786, 52.46385], [13.42786, 52.48445], [13.38272, 52.48445], [13.38272, 52.46385], ] ] polygon: Dict[str, Any] = { "type": "Polygon", "coordinates": coordinates, } multipolygon: Dict[str, Any] = { "type": "MultiPolygon", "coordinates": [coordinates], } geom_collection: Dict[str, Any] = { "type": "GeometryCollection", "geometries": [polygon, multipolygon], } test_feature: Dict[str, Any] = { "type": "Feature", "geometry": polygon, "properties": properties, "bbox": [13.38272, 52.46385, 13.42786, 52.48445], } test_feature_geom_null: Dict[str, Any] = { "type": "Feature", "geometry": None, "properties": properties, } test_feature_geometry_collection: Dict[str, Any] = { "type": "Feature", "geometry": geom_collection, "properties": properties, } @pytest.mark.parametrize( "obj", [ FeatureCollection, Feature, ], ) def test_pydantic_schema(obj): """Test schema for Pydantic Object.""" assert obj.model_json_schema() def test_feature_collection_iteration(): """test if feature collection is iterable""" gc = FeatureCollection( type="FeatureCollection", features=[test_feature, test_feature] ) assert hasattr(gc, "__geo_interface__") assert list(iter(gc)) assert len(list(gc.iter())) == 2 assert dict(gc) def test_geometry_collection_iteration(): """test if feature collection is iterable""" gc = FeatureCollection( type="FeatureCollection", features=[test_feature_geometry_collection] ) assert hasattr(gc, "__geo_interface__") assert list(iter(gc)) assert len(list(gc.iter())) == 1 assert dict(gc) def test_generic_properties_is_dict(): feature = Feature(**test_feature) assert hasattr(feature, "__geo_interface__") assert feature.properties["id"] == test_feature["properties"]["id"] assert isinstance(feature.properties, dict) assert not hasattr(feature.properties, "id") def test_generic_properties_is_dict_collection(): feature = Feature(**test_feature_geometry_collection) assert hasattr(feature, "__geo_interface__") assert ( feature.properties["id"] == test_feature_geometry_collection["properties"]["id"] ) assert isinstance(feature.properties, dict) assert not hasattr(feature.properties, "id") def test_generic_properties_is_object(): feature = Feature[Geometry, GenericProperties](**test_feature) assert feature.properties.id == test_feature["properties"]["id"] assert type(feature.properties) == GenericProperties assert hasattr(feature.properties, "id") def test_generic_geometry(): feature = Feature[Polygon, GenericProperties](**test_feature) assert feature.properties.id == test_feature_geometry_collection["properties"]["id"] assert type(feature.geometry) == Polygon assert type(feature.properties) == GenericProperties assert hasattr(feature.properties, "id") feature = Feature[Polygon, Dict](**test_feature) assert type(feature.geometry) == Polygon assert feature.properties["id"] == test_feature["properties"]["id"] assert isinstance(feature.properties, dict) assert not hasattr(feature.properties, "id") with pytest.raises(ValidationError): Feature[MultiPolygon, Dict](**({"type": "Feature", "geometry": polygon})) def test_generic_geometry_collection(): feature = Feature[GeometryCollection, GenericProperties]( **test_feature_geometry_collection ) assert feature.properties.id == test_feature_geometry_collection["properties"]["id"] assert type(feature.geometry) == GeometryCollection assert feature.geometry.wkt.startswith("GEOMETRYCOLLECTION (POLYGON ") assert type(feature.properties) == GenericProperties assert hasattr(feature.properties, "id") feature = Feature[GeometryCollection, Dict](**test_feature_geometry_collection) assert type(feature.geometry) == GeometryCollection assert ( feature.properties["id"] == test_feature_geometry_collection["properties"]["id"] ) assert isinstance(feature.properties, dict) assert not hasattr(feature.properties, "id") with pytest.raises(ValidationError): Feature[MultiPolygon, Dict](**({"type": "Feature", "geometry": polygon})) def test_generic_properties_should_raise_for_string(): with pytest.raises(ValidationError): Feature( **({"type": "Feature", "geometry": polygon, "properties": "should raise"}) ) def test_feature_collection_generic(): fc = FeatureCollection[Feature[Polygon, GenericProperties]]( type="FeatureCollection", features=[test_feature, test_feature] ) assert fc.length == 2 assert len(list(fc.iter())) == 2 assert type(fc.features[0].properties) == GenericProperties assert type(fc.features[0].geometry) == Polygon assert dict(fc) def test_geo_interface_protocol(): class Pointy: __geo_interface__ = {"type": "Point", "coordinates": (0.0, 0.0)} feat = Feature(type="Feature", geometry=Pointy(), properties={}) assert feat.geometry.model_dump(exclude_unset=True) == Pointy.__geo_interface__ def test_feature_with_null_geometry(): feature = Feature(**test_feature_geom_null) assert feature.geometry is None def test_feature_geo_interface_with_null_geometry(): feature = Feature(**test_feature_geom_null) assert "bbox" not in feature.__geo_interface__ def test_feature_collection_geo_interface_with_null_geometry(): fc = FeatureCollection( type="FeatureCollection", features=[test_feature_geom_null, test_feature] ) assert "bbox" not in fc.__geo_interface__ assert "bbox" not in fc.__geo_interface__["features"][0] assert "bbox" in fc.__geo_interface__["features"][1] @pytest.mark.parametrize("id", ["a", 1, "1"]) def test_feature_id(id): """Test if a string stays a string and if an int stays an int.""" feature = Feature(**test_feature, id=id) assert feature.id == id @pytest.mark.parametrize("id", [True, 1.0]) def test_bad_feature_id(id): """make sure it raises error.""" with pytest.raises(ValidationError): Feature(**test_feature, id=id) def test_feature_validation(): """Test default.""" assert Feature(type="Feature", properties=None, geometry=None) assert Feature(type="Feature", properties=None, geometry=None, bbox=None) with pytest.raises(ValidationError): # should be type=Feature Feature(type="feature", properties=None, geometry=None) with pytest.raises(ValidationError): # missing type Feature(properties=None, geometry=None) with pytest.raises(ValidationError): # missing properties Feature(type="Feature", geometry=None) with pytest.raises(ValidationError): # missing geometry Feature(type="Feature", properties=None) assert Feature( type="Feature", properties=None, bbox=(0, 0, 100, 100), geometry=None ) assert Feature( type="Feature", properties=None, bbox=(0, 0, 0, 100, 100, 100), geometry=None ) with pytest.raises(ValidationError): # bad bbox2d Feature(type="Feature", properties=None, bbox=(0, 100, 100, 0), geometry=None) with pytest.raises(ValidationError): # bad bbox3d Feature( type="Feature", properties=None, bbox=(0, 100, 100, 100, 0, 0), geometry=None, ) # Antimeridian with pytest.warns(UserWarning): Feature(type="Feature", properties=None, bbox=(100, 0, 0, 100), geometry=None) with pytest.warns(UserWarning): Feature( type="Feature", properties=None, bbox=(100, 0, 0, 0, 100, 100), geometry=None, ) def test_bbox_validation(): # Some attempts at generic validation did not validate the types within # bbox before passing them to the function and resulted in TypeErrors. # This test exists to ensure that doesn't happen in the future. with pytest.raises(ValidationError): Feature( type="Feature", properties=None, bbox=(0, "a", 0, 1, 1, 1), geometry=None, ) def test_feature_validation_error_count(): # Tests that validation does not include irrelevant errors to make them # easier to read. The input below used to raise 18 errors. # See #93 for more details. with pytest.raises(ValidationError): try: Feature( type="Feature", geometry=Polygon( type="Polygon", coordinates=[ [ (-55.9947406591177, -9.26104045526505), (-55.9976752102375, -9.266589696568962), (-56.00200328975916, -9.264041751931352), (-55.99899921566248, -9.257935213034594), (-55.99477406591177, -9.26103945526505), ] ], ), properties={}, ) except ValidationError as e: assert e.error_count() == 1 raise def test_feature_serializer(): f = Feature( **{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": coordinates, }, "properties": {}, "id": "Yo", "bbox": [13.38272, 52.46385, 13.42786, 52.48445], } ) assert "bbox" in f.model_dump() assert "id" in f.model_dump() # Exclude assert "bbox" not in f.model_dump(exclude={"bbox"}) assert "bbox" not in list(json.loads(f.model_dump_json(exclude={"bbox"})).keys()) # Include assert ["bbox"] == list(f.model_dump(include={"bbox"}).keys()) assert ["bbox"] == list(json.loads(f.model_dump_json(include={"bbox"})).keys()) feat_ser = json.loads(f.model_dump_json()) assert "bbox" in feat_ser assert "id" in feat_ser assert "bbox" not in feat_ser["geometry"] f = Feature( **{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": coordinates, }, "properties": {}, } ) # BBOX Should'nt be present if `None` # https://github.com/developmentseed/geojson-pydantic/issues/125 assert "bbox" in f.model_dump() feat_ser = json.loads(f.model_dump_json()) assert "bbox" not in feat_ser assert "id" not in feat_ser assert "bbox" not in feat_ser["geometry"] f = Feature( **{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": coordinates, "bbox": [13.38272, 52.46385, 13.42786, 52.48445], }, "properties": {}, } ) feat_ser = json.loads(f.model_dump_json()) assert "bbox" not in feat_ser assert "id" not in feat_ser assert "bbox" in feat_ser["geometry"] def test_feature_collection_serializer(): fc = FeatureCollection( **{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": coordinates, "bbox": [13.38272, 52.46385, 13.42786, 52.48445], }, "properties": {}, "bbox": [13.38272, 52.46385, 13.42786, 52.48445], } ], "bbox": [13.38272, 52.46385, 13.42786, 52.48445], } ) assert "bbox" in fc.model_dump() # Exclude assert "bbox" not in fc.model_dump(exclude={"bbox"}) assert "bbox" not in list(json.loads(fc.model_dump_json(exclude={"bbox"})).keys()) # Include assert ["bbox"] == list(fc.model_dump(include={"bbox"}).keys()) assert ["bbox"] == list(json.loads(fc.model_dump_json(include={"bbox"})).keys()) featcoll_ser = json.loads(fc.model_dump_json()) assert "bbox" in featcoll_ser assert "bbox" in featcoll_ser["features"][0] assert "bbox" in featcoll_ser["features"][0]["geometry"] fc = FeatureCollection( **{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": coordinates, }, "properties": {}, } ], } ) assert "bbox" in fc.model_dump() featcoll_ser = json.loads(fc.model_dump_json()) assert "bbox" not in featcoll_ser assert "bbox" not in featcoll_ser["features"][0] assert "bbox" not in featcoll_ser["features"][0]["geometry"] geojson-pydantic-2.0.0/tests/test_geometries.py000066400000000000000000000700551500622330400217060ustar00rootroot00000000000000import json import pytest import shapely from pydantic import ValidationError from geojson_pydantic.geometries import ( Geometry, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, parse_geometry_obj, ) @pytest.mark.parametrize( "obj", [ GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ], ) def test_pydantic_schema(obj): """Test schema for Pydantic Object.""" assert obj.model_json_schema() @pytest.mark.parametrize("coordinates", [(1.01, 2.01), (1.0, 2.0, 3.0), (1.0, 2.0)]) def test_point_valid_coordinates(coordinates): """ Two or three number elements as coordinates should be okay """ p = Point(type="Point", coordinates=coordinates) assert p.type == "Point" assert p.coordinates == coordinates assert hasattr(p, "__geo_interface__") @pytest.mark.parametrize( "coordinates", [(1.0,), (1.0, 2.0, 3.0, 4.0), "Foo", (None, 2.0), (1.0, (2.0,)), (), [], None], ) def test_point_invalid_coordinates(coordinates): """ Too few or to many elements should not, nor weird data types """ with pytest.raises(ValidationError): Point(type="Point", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Empty array [], # No Z [(1.0, 2.0)], [(1.0, 2.0), (1.0, 2.0)], # Has Z [(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)], # Mixed [(1.0, 2.0), (1.0, 2.0, 3.0)], ], ) def test_multi_point_valid_coordinates(coordinates): """ Two or three number elements as coordinates should be okay, as well as an empty array. """ p = MultiPoint(type="MultiPoint", coordinates=coordinates) assert p.type == "MultiPoint" assert p.coordinates == coordinates assert hasattr(p, "__geo_interface__") @pytest.mark.parametrize( "coordinates", [[(1.0,)], [(1.0, 2.0, 3.0, 4.0)], ["Foo"], [(None, 2.0)], [(1.0, (2.0,))], None], ) def test_multi_point_invalid_coordinates(coordinates): """ Too few or to many elements should not, nor weird data types """ with pytest.raises(ValidationError): MultiPoint(type="MultiPoint", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Two Points, no Z [(1.0, 2.0), (3.0, 4.0)], # Three Points, no Z [(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)], # Two Points, has Z [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)], # Shapely doesn't like mixed here ], ) def test_line_string_valid_coordinates(coordinates): """ A list of two coordinates or more should be okay """ linestring = LineString(type="LineString", coordinates=coordinates) assert linestring.type == "LineString" assert linestring.coordinates == coordinates assert hasattr(linestring, "__geo_interface__") @pytest.mark.parametrize("coordinates", [None, "Foo", [], [(1.0, 2.0)], ["Foo", "Bar"]]) def test_line_string_invalid_coordinates(coordinates): """ But we don't accept non-list inputs, too few coordinates, or bogus coordinates """ with pytest.raises(ValidationError): LineString(type="LineString", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Empty array [], # One line, two points, no Z [[(1.0, 2.0), (3.0, 4.0)]], # One line, two points, has Z [[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]], # One line, three points, no Z [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)]], # Two lines, two points each, no Z [[(1.0, 2.0), (3.0, 4.0)], [(0.0, 0.0), (1.0, 1.0)]], # Two lines, two points each, has Z [[(1.0, 2.0, 0.0), (3.0, 4.0, 1.0)], [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]], # Mixed [[(1.0, 2.0), (3.0, 4.0)], [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]], ], ) def test_multi_line_string_valid_coordinates(coordinates): """ A list of two coordinates or more should be okay """ multilinestring = MultiLineString(type="MultiLineString", coordinates=coordinates) assert multilinestring.type == "MultiLineString" assert multilinestring.coordinates == coordinates assert hasattr(multilinestring, "__geo_interface__") @pytest.mark.parametrize( "coordinates", [None, [None], ["Foo"], [[]], [[(1.0, 2.0)]], [["Foo", "Bar"]]] ) def test_multi_line_string_invalid_coordinates(coordinates): """ But we don't accept non-list inputs, too few coordinates, or bogus coordinates """ with pytest.raises(ValidationError): MultiLineString(type="MultiLineString", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Empty array [], # Polygon, no Z [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]], # Polygon, has Z [[(0.0, 0.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 0.0)]], ], ) def test_polygon_valid_coordinates(coordinates): """ Should accept lists of linear rings """ polygon = Polygon(type="Polygon", coordinates=coordinates) assert polygon.type == "Polygon" assert polygon.coordinates == coordinates assert hasattr(polygon, "__geo_interface__") if polygon.coordinates: assert polygon.exterior == coordinates[0] else: assert polygon.exterior is None assert not list(polygon.interiors) @pytest.mark.parametrize( "coordinates", [ # Polygon with holes, no Z [ [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)], [(2.0, 2.0), (2.0, 4.0), (4.0, 4.0), (4.0, 2.0), (2.0, 2.0)], ], # Polygon with holes, has Z [ [ (0.0, 0.0, 0.0), (0.0, 10.0, 0.0), (10.0, 10.0, 0.0), (10.0, 0.0, 0.0), (0.0, 0.0, 0.0), ], [ (2.0, 2.0, 1.0), (2.0, 4.0, 1.0), (4.0, 4.0, 1.0), (4.0, 2.0, 1.0), (2.0, 2.0, 1.0), ], ], # Mixed [ [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)], [ (2.0, 2.0, 2.0), (2.0, 4.0, 0.0), (4.0, 4.0, 0.0), (4.0, 2.0, 0.0), (2.0, 2.0, 2.0), ], ], ], ) def test_polygon_with_holes(coordinates): """Check interior and exterior rings.""" polygon = Polygon(type="Polygon", coordinates=coordinates) assert polygon.type == "Polygon" assert hasattr(polygon, "__geo_interface__") assert polygon.exterior == polygon.coordinates[0] assert list(polygon.interiors) == [polygon.coordinates[1]] @pytest.mark.parametrize( "coordinates", [ "foo", None, [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)], "foo", None], [[(1.0, 2.0), (3.0, 4.0), (1.0, 2.0)]], [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (7.0, 8.0)]], ], ) def test_polygon_invalid_coordinates(coordinates): """ Should not accept when: - Coordinates is not a list - Not all elements in coordinates are lists - If not all elements have four or more coordinates - If not all elements are linear rings """ with pytest.raises(ValidationError): Polygon(type="Polygon", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Empty array [], # Multipolygon, no Z [ [ [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)], [(2.1, 2.1), (2.2, 2.1), (2.2, 2.2), (2.1, 2.2), (2.1, 2.1)], ] ], # Multipolygon, has Z [ [ [ (0.0, 0.0, 4.0), (1.0, 0.0, 4.0), (1.0, 1.0, 4.0), (0.0, 1.0, 4.0), (0.0, 0.0, 4.0), ], [ (2.1, 2.1, 4.0), (2.2, 2.1, 4.0), (2.2, 2.2, 4.0), (2.1, 2.2, 4.0), (2.1, 2.1, 4.0), ], ] ], # Mixed [ [ [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)], [ (2.1, 2.1, 2.1), (2.2, 2.1, 2.0), (2.2, 2.2, 2.2), (2.1, 2.2, 2.3), (2.1, 2.1, 2.1), ], ] ], ], ) def test_multi_polygon(coordinates): """Should accept sequence of polygons.""" multi_polygon = MultiPolygon(type="MultiPolygon", coordinates=coordinates) assert multi_polygon.type == "MultiPolygon" assert hasattr(multi_polygon, "__geo_interface__") @pytest.mark.parametrize( "coordinates", [ "foo", None, [ [ [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)], ], [ [(2.1, 2.1), (2.2, 2.1), (2.2, 2.2), (2.1, 4.2)], ], ], ], ) def test_multipolygon_invalid_coordinates(coordinates): with pytest.raises(ValidationError): MultiPolygon(type="MultiPolygon", coordinates=coordinates) def test_parse_geometry_obj_point(): assert parse_geometry_obj({"type": "Point", "coordinates": [102.0, 0.5]}) == Point( type="Point", coordinates=(102.0, 0.5) ) @pytest.mark.parametrize( "geojson_pydantic_model", ( GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ), ) def test_schema_consistency(geojson_pydantic_model): """Test to check that the schema is the same for validation and serialization""" assert geojson_pydantic_model.model_json_schema( mode="validation" ) == geojson_pydantic_model.model_json_schema(mode="serialization") def test_parse_geometry_obj_multi_point(): assert parse_geometry_obj( {"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]} ) == MultiPoint(type="MultiPoint", coordinates=[(100.0, 0.0), (101.0, 1.0)]) def test_parse_geometry_obj_line_string(): assert parse_geometry_obj( { "type": "LineString", "coordinates": [[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]], } ) == LineString( type="LineString", coordinates=[(102.0, 0.0), (103.0, 1.0), (104.0, 0.0), (105.0, 1.0)], ) def test_parse_geometry_obj_multi_line_string(): assert parse_geometry_obj( { "type": "MultiLineString", "coordinates": [[[100.0, 0.0], [101.0, 1.0]], [[102.0, 2.0], [103.0, 3.0]]], } ) == MultiLineString( type="MultiLineString", coordinates=[[(100.0, 0.0), (101.0, 1.0)], [(102.0, 2.0), (103.0, 3.0)]], ) def test_parse_geometry_obj_polygon(): assert parse_geometry_obj( { "type": "Polygon", "coordinates": [ [[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]] ], } ) == Polygon( type="Polygon", coordinates=[ [(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)] ], ) def test_parse_geometry_obj_multi_polygon(): assert parse_geometry_obj( { "type": "MultiPolygon", "coordinates": [ [ [ [102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0], ] ], [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0], ], [ [100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2], ], ], ], } ) == MultiPolygon( type="MultiPolygon", coordinates=[ [[(102.0, 2.0), (103.0, 2.0), (103.0, 3.0), (102.0, 3.0), (102.0, 2.0)]], [ [(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)], [(100.2, 0.2), (100.8, 0.2), (100.8, 0.8), (100.2, 0.8), (100.2, 0.2)], ], ], ) def test_parse_geometry_obj_geometry_collection(): assert parse_geometry_obj( { "type": "GeometryCollection", "geometries": [ {"type": "Point", "coordinates": [102.0, 0.5]}, {"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]}, ], } ) == GeometryCollection( type="GeometryCollection", geometries=[ Point(type="Point", coordinates=(102.0, 0.5)), MultiPoint(type="MultiPoint", coordinates=[(100.0, 0.0), (101.0, 1.0)]), ], ) def test_parse_geometry_obj_invalid_type(): with pytest.raises(ValueError): parse_geometry_obj({"type": "This type", "obviously": "doesn't exist"}) with pytest.raises(ValueError): parse_geometry_obj({"type": "", "obviously": "doesn't exist"}) with pytest.raises(ValueError): parse_geometry_obj({}) def test_parse_geometry_obj_invalid_point(): """ litmus test that invalid geometries don't get parsed """ with pytest.raises(ValidationError): parse_geometry_obj( {"type": "Point", "coordinates": ["not", "valid", "coordinates"]} ) @pytest.mark.parametrize( "coordinates", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]] ) def test_geometry_collection_iteration(coordinates): """test if geometry collection is iterable""" polygon = Polygon(type="Polygon", coordinates=coordinates) multipolygon = MultiPolygon(type="MultiPolygon", coordinates=[coordinates]) gc = GeometryCollection( type="GeometryCollection", geometries=[polygon, multipolygon] ) assert hasattr(gc, "__geo_interface__") assert len(list(gc.iter())) == 2 assert list(iter(gc)) @pytest.mark.parametrize( "coordinates", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]] ) def test_len_geometry_collection(coordinates): """test if GeometryCollection return self leng""" polygon = Polygon(type="Polygon", coordinates=coordinates) multipolygon = MultiPolygon(type="MultiPolygon", coordinates=[coordinates]) gc = GeometryCollection( type="GeometryCollection", geometries=[polygon, multipolygon] ) assert gc.length == 2 assert len(list(gc.iter())) == 2 assert dict(gc) assert list(iter(gc)) @pytest.mark.parametrize( "coordinates", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]] ) def test_getitem_geometry_collection(coordinates): """test if GeometryCollection is subscriptable""" polygon = Polygon(type="Polygon", coordinates=coordinates) multipolygon = MultiPolygon(type="MultiPolygon", coordinates=[coordinates]) gc = GeometryCollection( type="GeometryCollection", geometries=[polygon, multipolygon] ) assert polygon == gc.geometries[0] assert multipolygon == gc.geometries[1] def test_wkt_mixed_geometry_collection(): point = Point(type="Point", coordinates=(0.0, 0.0, 0.0)) line_string = LineString( type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] ) assert ( GeometryCollection( type="GeometryCollection", geometries=[point, line_string] ).wkt == "GEOMETRYCOLLECTION Z (POINT Z (0.0 0.0 0.0), LINESTRING Z (0.0 0.0 0.0, 1.0 1.0 1.0))" ) def test_wkt_empty_geometry_collection(): assert ( GeometryCollection(type="GeometryCollection", geometries=[]).wkt == "GEOMETRYCOLLECTION EMPTY" ) def test_geometry_collection_warnings(): point = Point(type="Point", coordinates=(0.0, 0.0, 0.0)) line_string_z = LineString( type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] ) line_string = LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]) # one geometry with pytest.warns( UserWarning, match="GeometryCollection should not be used for single geometries.", ): GeometryCollection( type="GeometryCollection", geometries=[ point, ], ) # collections of collections with pytest.warns( UserWarning, match="GeometryCollection should not be used for nested GeometryCollections.", ): GeometryCollection( type="GeometryCollection", geometries=[ GeometryCollection( type="GeometryCollection", geometries=[point, line_string_z] ), point, ], ) # homogeneous (Z) geometry with pytest.raises(ValidationError): GeometryCollection(type="GeometryCollection", geometries=[point, line_string]) def test_polygon_from_bounds(): """Result from `from_bounds` class method should be the same.""" coordinates = [[(1.0, 2.0), (3.0, 2.0), (3.0, 4.0), (1.0, 4.0), (1.0, 2.0)]] assert Polygon(type="Polygon", coordinates=coordinates) == Polygon.from_bounds( 1.0, 2.0, 3.0, 4.0 ) def test_wkt_name(): """Make sure WKT name is derived from geometry Type.""" class PointType(Point): ... assert ( PointType(type="Point", coordinates=(1.01, 2.01)).wkt == Point(type="Point", coordinates=(1.01, 2.01)).wkt ) @pytest.mark.parametrize( "coordinates,expected", [ ((0, 0), False), ((0, 0, 0), True), ], ) def test_point_has_z(coordinates, expected): assert Point(type="Point", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "coordinates,expected", [ ([(0, 0)], False), ([(0, 0), (1, 1)], False), ([(0, 0), (1, 1, 1)], True), ([(0, 0, 0)], True), ([(0, 0, 0), (1, 1)], True), ], ) def test_multipoint_has_z(coordinates, expected): assert MultiPoint(type="MultiPoint", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "coordinates,expected", [ ([(0, 0), (1, 1)], False), ([(0, 0), (1, 1, 1)], True), ([(0, 0, 0), (1, 1, 1)], True), ([(0, 0, 0), (1, 1)], True), ], ) def test_linestring_has_z(coordinates, expected): assert LineString(type="LineString", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "coordinates,expected", [ ([[(0, 0), (1, 1)]], False), ([[(0, 0), (1, 1)], [(0, 0), (1, 1)]], False), ([[(0, 0), (1, 1)], [(0, 0, 0), (1, 1)]], True), ([[(0, 0), (1, 1)], [(0, 0), (1, 1, 1)]], True), ([[(0, 0), (1, 1, 1)]], True), ([[(0, 0, 0), (1, 1, 1)]], True), ([[(0, 0, 0), (1, 1)]], True), ([[(0, 0, 0), (1, 1, 1)], [(0, 0, 0), (1, 1, 1)]], True), ], ) def test_multilinestring_has_z(coordinates, expected): assert ( MultiLineString(type="MultiLineString", coordinates=coordinates).has_z == expected ) @pytest.mark.parametrize( "coordinates,expected", [ ([[(0, 0), (1, 1), (2, 2), (0, 0)]], False), ([[(0, 0), (1, 1), (2, 2, 2), (0, 0)]], True), ([[(0, 0), (1, 1), (2, 2), (0, 0)], [(0, 0), (1, 1), (2, 2), (0, 0)]], False), ( [[(0, 0), (1, 1), (2, 2), (0, 0)], [(0, 0), (1, 1), (2, 2, 2), (0, 0)]], True, ), ([[(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)]], True), ( [ [(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)], [(0, 0), (1, 1), (2, 2), (0, 0)], ], True, ), ], ) def test_polygon_has_z(coordinates, expected): assert Polygon(type="Polygon", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "coordinates,expected", [ ([[[(0, 0), (1, 1), (2, 2), (0, 0)]]], False), ([[[(0, 0), (1, 1), (2, 2, 2), (0, 0)]]], True), ( [[[(0, 0), (1, 1), (2, 2), (0, 0)]], [[(0, 0), (1, 1), (2, 2), (0, 0)]]], False, ), ( [ [[(0, 0), (1, 1), (2, 2), (0, 0)]], [ [(0, 0), (1, 1), (2, 2), (0, 0)], [(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)], ], ], True, ), ( [[[(0, 0), (1, 1), (2, 2), (0, 0)]], [[(0, 0), (1, 1), (2, 2, 2), (0, 0)]]], True, ), ([[[(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)]]], True), ( [ [[(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)]], [[(0, 0), (1, 1), (2, 2), (0, 0)]], ], True, ), ], ) def test_multipolygon_has_z(coordinates, expected): assert MultiPolygon(type="MultiPolygon", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "shape", [ MultiPoint, MultiLineString, Polygon, MultiPolygon, ], ) def test_wkt_empty(shape): assert shape(type=shape.__name__, coordinates=[]).wkt.endswith(" EMPTY") def test_wkt_empty_geometrycollection(): assert GeometryCollection(type="GeometryCollection", geometries=[]).wkt.endswith( " EMPTY" ) @pytest.mark.parametrize( "wkt", ( "POINT (0.0 0.0)", # "POINT EMPTY" does not result in valid GeoJSON "POINT Z (0.0 0.0 0.0)", "MULTIPOINT ((0.0 0.0))", "MULTIPOINT Z ((0.0 0.0 0.0))", "MULTIPOINT ((0.0 0.0), (1.0 1.0))", "MULTIPOINT Z ((0.0 0.0 0.0), (1.0 1.0 1.0))", "MULTIPOINT EMPTY", "LINESTRING (0.0 0.0, 1.0 1.0, 2.0 2.0)", "LINESTRING Z (0.0 0.0 0.0, 1.0 1.0 1.0, 2.0 2.0 2.0)", # "LINESTRING EMPTY" does not result in valid GeoJSON "MULTILINESTRING ((0.0 0.0, 1.0 1.0))", "MULTILINESTRING ((0.0 0.0, 1.0 1.0), (1.0 1.0, 2.0 2.0))", "MULTILINESTRING Z ((0.0 0.0 0.0, 1.0 1.0 1.0))", "MULTILINESTRING Z ((0.0 0.0 0.0, 1.0 1.0 1.0), (1.0 1.0 1.0, 2.0 2.0 2.0))", "MULTILINESTRING EMPTY", "POLYGON ((0.0 0.0, 1.0 1.0, 2.0 2.0, 3.0 3.0, 0.0 0.0))", "POLYGON ((0.0 0.0, 4.0 0.0, 4.0 4.0, 0.0 4.0, 0.0 0.0), (1.0 1.0, 1.0 2.0, 2.0 2.0, 2.0 1.0, 1.0 1.0))", "POLYGON Z ((0.0 0.0 0.0, 1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 0.0 0.0 0.0))", "POLYGON Z ((0.0 0.0 0.0, 4.0 0.0 0.0, 4.0 4.0 0.0, 0.0 4.0 0.0, 0.0 0.0 0.0), (1.0 1.0 0.0, 1.0 2.0 0.0, 2.0 2.0 0.0, 2.0 1.0 0.0, 1.0 1.0 0.0))", "POLYGON EMPTY", "MULTIPOLYGON (((0.0 0.0, 1.0 1.0, 2.0 2.0, 3.0 3.0, 0.0 0.0)))", "MULTIPOLYGON (((0.0 0.0, 1.0 1.0, 2.0 2.0, 3.0 3.0, 0.0 0.0)), ((1.0 1.0, 2.0 2.0, 3.0 3.0, 4.0 4.0, 1.0 1.0)))", "MULTIPOLYGON Z (((0.0 0.0 0.0, 1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 0.0 0.0 0.0)))", "MULTIPOLYGON Z (((0.0 0.0 0.0, 1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 0.0 0.0 0.0)), ((1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 4.0 4.0 0.0, 1.0 1.0 0.0)))", "MULTIPOLYGON EMPTY", "GEOMETRYCOLLECTION (POINT (0.0 0.0))", "GEOMETRYCOLLECTION (POLYGON EMPTY, MULTIPOLYGON (((0.0 0.0, 1.0 1.0, 2.0 2.0, 3.0 3.0, 0.0 0.0))))", "GEOMETRYCOLLECTION (POINT (0.0 0.0), MULTIPOINT ((0.0 0.0), (1.0 1.0)))", "GEOMETRYCOLLECTION Z (POLYGON Z ((0.0 0.0 0.0, 1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 0.0 0.0 0.0)), MULTIPOLYGON Z (((0.0 0.0 0.0, 1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 0.0 0.0 0.0))))", "GEOMETRYCOLLECTION EMPTY", ), ) def test_wkt(wkt: str): # Use Shapely to parse the input WKT so we know it is parsable by other tools. # Then load it into a Geometry and ensure the output WKT is the same as the input. assert parse_geometry_obj(shapely.from_wkt(wkt).__geo_interface__).wkt == wkt @pytest.mark.parametrize( "geom", ( Point(type="Point", coordinates=[0, 0], bbox=[0, 0, 0, 0]), Point(type="Point", coordinates=[0, 0]), MultiPoint(type="MultiPoint", coordinates=[(0.0, 0.0)], bbox=[0, 0, 0, 0]), MultiPoint(type="MultiPoint", coordinates=[(0.0, 0.0)]), LineString( type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)], bbox=[0, 0, 1, 1] ), LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]), MultiLineString( type="MultiLineString", coordinates=[[(0.0, 0.0), (1.0, 1.0)]], bbox=[0, 0, 1, 1], ), MultiLineString(type="MultiLineString", coordinates=[[(0.0, 0.0), (1.0, 1.0)]]), Polygon( type="Polygon", coordinates=[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]], bbox=[1.0, 2.0, 5.0, 6.0], ), Polygon( type="Polygon", coordinates=[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]], ), MultiPolygon( type="MultiPolygon", coordinates=[[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]], bbox=[1.0, 2.0, 5.0, 6.0], ), MultiPolygon( type="MultiPolygon", coordinates=[[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]], ), ), ) def test_geometry_serializer(geom: Geometry): # bbox should always be in the dictionary version of the model # but should only be in the JSON version if not None assert "bbox" in geom.model_dump() if geom.bbox is not None: assert "bbox" in geom.model_dump_json() else: assert "bbox" not in geom.model_dump_json() def test_geometry_collection_serializer(): geom = GeometryCollection( type="GeometryCollection", geometries=[ Point(type="Point", coordinates=[0, 0]), LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]), ], ) assert not geom.has_z # bbox will be in the Dict assert "bbox" in geom.model_dump() assert "bbox" in geom.model_dump()["geometries"][0] geom = GeometryCollection( type="GeometryCollection", geometries=[ Point(type="Point", coordinates=[0, 0, 0]), LineString( type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] ), ], ) assert geom.has_z # bbox should not be in any Geometry nor at the top level geom_ser = json.loads(geom.model_dump_json()) assert "bbox" not in geom_ser assert "bbox" not in geom_ser["geometries"][0] assert "bbox" not in geom_ser["geometries"][0] geom = GeometryCollection( type="GeometryCollection", geometries=[ Point(type="Point", coordinates=[0, 0], bbox=[0, 0, 0, 0]), LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]), ], ) # bbox not in the top level but in the first geometry (point) geom_ser = json.loads(geom.model_dump_json()) assert "bbox" not in geom_ser assert "bbox" in geom_ser["geometries"][0] assert "bbox" not in geom_ser["geometries"][1] geom = GeometryCollection( type="GeometryCollection", geometries=[ Point(type="Point", coordinates=[0, 0], bbox=[0, 0, 0, 0]), LineString( type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)], bbox=[0, 0, 1, 1], ), ], ) geom_ser = json.loads(geom.model_dump_json()) assert "bbox" not in geom_ser assert "bbox" in geom_ser["geometries"][0] assert "bbox" in geom_ser["geometries"][1] geom = GeometryCollection( type="GeometryCollection", geometries=[ Point(type="Point", coordinates=[0, 0]), LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]), ], bbox=[0, 0, 1, 1], ) geom_ser = json.loads(geom.model_dump_json()) assert "bbox" in geom_ser assert "bbox" not in geom_ser["geometries"][0] assert "bbox" not in geom_ser["geometries"][1] geojson-pydantic-2.0.0/tests/test_package.py000066400000000000000000000005771500622330400211400ustar00rootroot00000000000000import geojson_pydantic def test_import_namespace(): """We have exposed all of the public objects via __all__""" assert sorted(geojson_pydantic.__all__) == [ "Feature", "FeatureCollection", "GeometryCollection", "LineString", "MultiLineString", "MultiPoint", "MultiPolygon", "Point", "Polygon", ] geojson-pydantic-2.0.0/tests/test_types.py000066400000000000000000000030421500622330400206770ustar00rootroot00000000000000import pytest from geojson_pydantic.types import Position2D, Position3D @pytest.mark.parametrize("coordinates", [(1, 2), (1.0, 2.0), (1.01, 2.01)]) def test_position2d_valid_coordinates(coordinates): """ Two number elements as coordinates should be okay """ p = Position2D(longitude=coordinates[0], latitude=coordinates[1]) assert p[0] == coordinates[0] assert p[1] == coordinates[1] assert p.longitude == coordinates[0] assert p.latitude == coordinates[1] assert p == coordinates p = Position2D(*coordinates) assert p[0] == coordinates[0] assert p[1] == coordinates[1] assert p.longitude == coordinates[0] assert p.latitude == coordinates[1] assert p == coordinates @pytest.mark.parametrize( "coordinates", [(1, 2, 3), (1.0, 2.0, 3.0), (1.01, 2.01, 3.01)] ) def test_position3d_valid_coordinates(coordinates): """ Three number elements as coordinates should be okay """ p = Position3D( longitude=coordinates[0], latitude=coordinates[1], altitude=coordinates[2] ) assert p[0] == coordinates[0] assert p[1] == coordinates[1] assert p[2] == coordinates[2] assert p.longitude == coordinates[0] assert p.latitude == coordinates[1] assert p.altitude == coordinates[2] assert p == coordinates p = Position3D(*coordinates) assert p[0] == coordinates[0] assert p[1] == coordinates[1] assert p[2] == coordinates[2] assert p.longitude == coordinates[0] assert p.latitude == coordinates[1] assert p.altitude == coordinates[2]