gpkglite 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gpkglite-0.1.0/CHANGELOG.md +35 -0
- gpkglite-0.1.0/CITATION.cff +20 -0
- gpkglite-0.1.0/LICENSE +21 -0
- gpkglite-0.1.0/MANIFEST.in +10 -0
- gpkglite-0.1.0/PKG-INFO +251 -0
- gpkglite-0.1.0/README.md +212 -0
- gpkglite-0.1.0/examples/quickstart.py +59 -0
- gpkglite-0.1.0/pyproject.toml +127 -0
- gpkglite-0.1.0/setup.cfg +4 -0
- gpkglite-0.1.0/src/gpkglite/__init__.py +79 -0
- gpkglite-0.1.0/src/gpkglite/__main__.py +7 -0
- gpkglite-0.1.0/src/gpkglite/_sql.py +187 -0
- gpkglite-0.1.0/src/gpkglite/binary.py +219 -0
- gpkglite-0.1.0/src/gpkglite/cli.py +210 -0
- gpkglite-0.1.0/src/gpkglite/convert.py +659 -0
- gpkglite-0.1.0/src/gpkglite/crs.py +155 -0
- gpkglite-0.1.0/src/gpkglite/errors.py +39 -0
- gpkglite-0.1.0/src/gpkglite/geopackage.py +1604 -0
- gpkglite-0.1.0/src/gpkglite/py.typed +0 -0
- gpkglite-0.1.0/src/gpkglite/validate.py +673 -0
- gpkglite-0.1.0/src/gpkglite/wkb.py +437 -0
- gpkglite-0.1.0/src/gpkglite/wkt.py +58 -0
- gpkglite-0.1.0/src/gpkglite.egg-info/PKG-INFO +251 -0
- gpkglite-0.1.0/src/gpkglite.egg-info/SOURCES.txt +40 -0
- gpkglite-0.1.0/src/gpkglite.egg-info/dependency_links.txt +1 -0
- gpkglite-0.1.0/src/gpkglite.egg-info/entry_points.txt +2 -0
- gpkglite-0.1.0/src/gpkglite.egg-info/requires.txt +10 -0
- gpkglite-0.1.0/src/gpkglite.egg-info/top_level.txt +1 -0
- gpkglite-0.1.0/tests/__init__.py +0 -0
- gpkglite-0.1.0/tests/conftest.py +107 -0
- gpkglite-0.1.0/tests/data/README.md +14 -0
- gpkglite-0.1.0/tests/data/gdal_curves.gpkg +0 -0
- gpkglite-0.1.0/tests/data/gdal_vectors.gpkg +0 -0
- gpkglite-0.1.0/tests/test_binary.py +130 -0
- gpkglite-0.1.0/tests/test_cli.py +184 -0
- gpkglite-0.1.0/tests/test_convert.py +275 -0
- gpkglite-0.1.0/tests/test_gdal_fixtures.py +136 -0
- gpkglite-0.1.0/tests/test_geopackage.py +524 -0
- gpkglite-0.1.0/tests/test_interop.py +154 -0
- gpkglite-0.1.0/tests/test_rtree.py +132 -0
- gpkglite-0.1.0/tests/test_validate.py +287 -0
- gpkglite-0.1.0/tests/test_wkb.py +222 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `gpkglite.open()` / `gpkglite.create()`: read and write OGC GeoPackage 1.3.1
|
|
12
|
+
(12-128r18) files with the standard library only (`application_id` GPKG,
|
|
13
|
+
`user_version` 10301, required tables and spatial reference systems).
|
|
14
|
+
- Layer listing, schema, CRS (`gpkg_spatial_ref_sys`, including WKT2
|
|
15
|
+
`definition_12_063`) and extent inspection.
|
|
16
|
+
- Feature iteration as GeoJSON-like Features or flat rows with SQL `where`
|
|
17
|
+
filters and bound parameters, RTree-backed `bbox` filters, paging, ordering,
|
|
18
|
+
column selection and several geometry output formats.
|
|
19
|
+
- StandardGeoPackageBinary encoder/decoder (both byte orders, envelope types
|
|
20
|
+
0-4, empty flag) and an ISO WKB reader/writer for all OGC simple feature types
|
|
21
|
+
in XY, XYZ, XYM and XYZM; readable errors for curve types and
|
|
22
|
+
ExtendedGeoPackageBinary.
|
|
23
|
+
- Feature tables with typed columns, attribute-only tables, insert / update /
|
|
24
|
+
delete, extent maintenance, RTree spatial index with the standard triggers
|
|
25
|
+
(1.3 and 1.4 trigger sets), metadata extension, CRS WKT extension.
|
|
26
|
+
- `gpkglite.validate()`: structural, referential, geometry-blob and RTree checks
|
|
27
|
+
with requirement numbers.
|
|
28
|
+
- Conversion helpers: GeoJSON to/from GeoPackage, CSV points to GeoPackage,
|
|
29
|
+
layer to CSV, and optional GeoPandas interop (`to_geodataframe`,
|
|
30
|
+
`from_geodataframe`).
|
|
31
|
+
- Command line interface: `gpkglite info | dump | validate | from-geojson | from-csv`.
|
|
32
|
+
- Test fixtures written by GDAL/OGR and cross-validation tests against pyogrio,
|
|
33
|
+
`ogrinfo` and GDAL's `validate_gpkg.py`.
|
|
34
|
+
|
|
35
|
+
[0.1.0]: https://github.com/YusufEminoglu/gpkglite_sdk/releases/tag/v0.1.0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "yusufeminoglu@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "gpkglite"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-08"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/gpkglite_sdk"
|
|
13
|
+
url: "https://pypi.org/project/gpkglite/"
|
|
14
|
+
keywords:
|
|
15
|
+
- geopackage
|
|
16
|
+
- gis
|
|
17
|
+
- sqlite
|
|
18
|
+
- wkb
|
|
19
|
+
- ogc
|
|
20
|
+
abstract: "Pure-Python reader, writer and validator for OGC GeoPackage files (12-128r18), using only the Python standard library."
|
gpkglite-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include CITATION.cff
|
|
5
|
+
include pyproject.toml
|
|
6
|
+
include src/gpkglite/py.typed
|
|
7
|
+
recursive-include src/gpkglite *.py
|
|
8
|
+
recursive-include tests *.py
|
|
9
|
+
recursive-include tests/data *.gpkg *.md
|
|
10
|
+
recursive-include examples *.py
|
gpkglite-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gpkglite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read, write and validate OGC GeoPackage files in pure Python: no GDAL, standard library only.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/gpkglite_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/gpkglite_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/gpkglite_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/gpkglite_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: geopackage,gpkg,gis,geospatial,sqlite,wkb,geojson,ogc,vector,rtree,spatial-index,serverless,pyodide,no-gdal,pure-python
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Intended Audience :: Developers
|
|
25
|
+
Classifier: Intended Audience :: Science/Research
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Provides-Extra: geo
|
|
31
|
+
Requires-Dist: shapely>=2.0.0; extra == "geo"
|
|
32
|
+
Requires-Dist: geopandas>=0.12.0; extra == "geo"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
37
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# gpkglite
|
|
41
|
+
|
|
42
|
+
**Read, write and validate OGC GeoPackage files in pure Python. No GDAL, no compiled
|
|
43
|
+
dependencies, standard library only (`sqlite3`, `struct`, `json`).**
|
|
44
|
+
|
|
45
|
+
[](https://github.com/YusufEminoglu/gpkglite_sdk/actions/workflows/ci.yml)
|
|
46
|
+

|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
GeoPackage (`.gpkg`) is the OGC standard container for vector data: an SQLite database
|
|
50
|
+
with a few well-defined tables and a binary geometry encoding. Reading or writing one
|
|
51
|
+
usually means installing GDAL (via `fiona`, `pyogrio`, `geopandas` or `osgeo`). That is
|
|
52
|
+
heavy, or impossible, in:
|
|
53
|
+
|
|
54
|
+
- serverless functions (AWS Lambda, Cloud Functions) with tight package size limits,
|
|
55
|
+
- slim Docker images and CI jobs,
|
|
56
|
+
- Pyodide / JupyterLite in the browser,
|
|
57
|
+
- Windows machines without OSGeo4W or conda,
|
|
58
|
+
- libraries that want to accept a `.gpkg` without pulling GDAL into their dependency tree.
|
|
59
|
+
|
|
60
|
+
`gpkglite` is a small, typed, dependency-free implementation of the parts of
|
|
61
|
+
[OGC 12-128r18 (GeoPackage 1.3.1)](https://www.geopackage.org/spec131/) that vector
|
|
62
|
+
workflows need. Files it writes open in GDAL/OGR, QGIS, geopandas/pyogrio and fiona,
|
|
63
|
+
and it reads files written by them.
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install gpkglite # standard library only
|
|
69
|
+
pip install "gpkglite[geo]" # + shapely / geopandas interop helpers
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick start
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import gpkglite
|
|
76
|
+
|
|
77
|
+
# write
|
|
78
|
+
with gpkglite.create("cities.gpkg") as gpkg:
|
|
79
|
+
layer = gpkg.create_layer(
|
|
80
|
+
"cities", "POINT", srs_id=4326,
|
|
81
|
+
fields={"name": "TEXT", "population": "INTEGER", "capital": "BOOLEAN"},
|
|
82
|
+
)
|
|
83
|
+
layer.insert({"type": "Point", "coordinates": [28.97, 41.01]},
|
|
84
|
+
{"name": "Istanbul", "population": 15_600_000, "capital": False})
|
|
85
|
+
layer.insert({"type": "Point", "coordinates": [32.85, 39.93]},
|
|
86
|
+
{"name": "Ankara", "population": 5_700_000, "capital": True})
|
|
87
|
+
|
|
88
|
+
# read
|
|
89
|
+
with gpkglite.open("cities.gpkg") as gpkg:
|
|
90
|
+
for info in gpkg.layers():
|
|
91
|
+
print(info.name, info.geometry_type, info.srs_id, info.bounds)
|
|
92
|
+
|
|
93
|
+
cities = gpkg["cities"]
|
|
94
|
+
print(cities.fields) # {'name': 'TEXT', 'population': 'INTEGER', 'capital': 'BOOLEAN'}
|
|
95
|
+
print(cities.crs.authority) # 'EPSG:4326'
|
|
96
|
+
for feature in cities.features(where="population > ?", params=[10_000_000]):
|
|
97
|
+
print(feature["properties"]["name"], feature["geometry"]["coordinates"])
|
|
98
|
+
|
|
99
|
+
# check it
|
|
100
|
+
report = gpkglite.validate("cities.gpkg")
|
|
101
|
+
print(report) # cities.gpkg: valid GeoPackage 1.3.1 (0 errors, ...)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Features
|
|
105
|
+
|
|
106
|
+
### Reading
|
|
107
|
+
|
|
108
|
+
- `gpkg.layers()`: every `gpkg_contents` entry (features and attribute tables) with
|
|
109
|
+
geometry column, geometry type, Z/M flags, SRS id and extent.
|
|
110
|
+
- `layer.columns`, `layer.fields`, `layer.schema`: column names and GeoPackage types.
|
|
111
|
+
- `gpkg.srs(id)`, `gpkg.list_srs()`, `layer.crs`: `gpkg_spatial_ref_sys` records,
|
|
112
|
+
including the WKT2 `definition_12_063` column when the CRS WKT extension is present.
|
|
113
|
+
- `layer.features(...)` yields GeoJSON-like `Feature` dicts; `layer.rows(...)` yields flat
|
|
114
|
+
dicts. Both accept:
|
|
115
|
+
- `where` + `params`: an SQL boolean expression with `?` or `:name` placeholders,
|
|
116
|
+
- `bbox=(minx, miny, maxx, maxy)`: uses the `rtree_<table>_<column>` index when present,
|
|
117
|
+
followed by an exact envelope test,
|
|
118
|
+
- `limit`, `offset`, `order_by`, `columns`,
|
|
119
|
+
- `geometry_format="geojson" | "wkb" | "gpkg" | "shapely"`,
|
|
120
|
+
- `on_error="raise" | "null" | "skip"` for geometries gpkglite cannot decode.
|
|
121
|
+
- `layer.count(...)`, `layer.get(fid)`, `layer.compute_bounds()`, `layer.to_geojson()`.
|
|
122
|
+
|
|
123
|
+
### Geometry encoding
|
|
124
|
+
|
|
125
|
+
- StandardGeoPackageBinary headers: magic, version, flags, both byte orders, all envelope
|
|
126
|
+
types (0 none, 1 XY, 2 XYZ, 3 XYM, 4 XYZM), the empty-geometry flag, SRS id.
|
|
127
|
+
- ISO WKB (and PostGIS extended WKB on input) for Point, LineString, Polygon, MultiPoint,
|
|
128
|
+
MultiLineString, MultiPolygon and GeometryCollection, in XY, XYZ, XYM and XYZM,
|
|
129
|
+
either byte order, nested collections, empty geometries (NaN points).
|
|
130
|
+
- Curve and surface types (CircularString, CompoundCurve, CurvePolygon, MultiCurve,
|
|
131
|
+
MultiSurface, PolyhedralSurface, TIN, Triangle) and ExtendedGeoPackageBinary raise
|
|
132
|
+
`UnsupportedGeometryError` with a readable message. Their raw WKB is still available
|
|
133
|
+
with `geometry_format="wkb"`, and their header envelope still drives `bbox` filters.
|
|
134
|
+
- Low-level API: `gpkglite.wkb.loads/dumps`, `gpkglite.encode_geometry`,
|
|
135
|
+
`gpkglite.decode_geometry`, `gpkglite.wkt.dumps`.
|
|
136
|
+
|
|
137
|
+
Geometries are plain dictionaries. Positions have 2, 3 or 4 numbers; a measured geometry
|
|
138
|
+
carries `"dims": "XYM"` (or `"XYZM"`) so it can be told apart from XYZ:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
{"type": "LineString", "coordinates": [[0, 0, 12.5], [10, 0, 13.0]]} # XYZ
|
|
142
|
+
{"type": "Point", "coordinates": [1, 2, 0.75], "dims": "XYM"} # XYM
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Any object with `__geo_interface__` (shapely, `geojson`, ...) is accepted as input.
|
|
146
|
+
|
|
147
|
+
### Writing
|
|
148
|
+
|
|
149
|
+
- `gpkglite.create(path)`: `application_id = 'GPKG'`, `user_version = 10301`, the required
|
|
150
|
+
`gpkg_spatial_ref_sys` rows (-1, 0, EPSG:4326), `gpkg_contents`,
|
|
151
|
+
`gpkg_geometry_columns`. `create(path, wkt2=True)` adds the CRS WKT extension.
|
|
152
|
+
- `gpkg.add_srs(...)`: built-in definitions for EPSG 4326, 3857, 4258, 4269 and all
|
|
153
|
+
WGS 84 / UTM zones (32601-32660, 32701-32760); any other CRS with an explicit WKT.
|
|
154
|
+
- `gpkg.create_layer(name, geometry_type, srs_id, fields, z=0|1|2, m=0|1|2,
|
|
155
|
+
spatial_index=True)` and `gpkg.create_table(name, fields)` for attribute-only tables.
|
|
156
|
+
Field types: `BOOLEAN, TINYINT, SMALLINT, MEDIUMINT, INT, INTEGER, FLOAT, DOUBLE, REAL,
|
|
157
|
+
TEXT, TEXT(n), BLOB, BLOB(n), DATE, DATETIME`, or Python types (`int`, `str`, `bool`,
|
|
158
|
+
`float`, `bytes`, `datetime.date`, `datetime.datetime`).
|
|
159
|
+
- `layer.insert(...)`, `layer.write_features(...)` (one transaction), `layer.update(...)`,
|
|
160
|
+
`layer.delete(fid)`, `layer.delete_where(...)`, `layer.add_field(...)`. Geometry type
|
|
161
|
+
and Z/M constraints of the layer are enforced; `date`/`datetime` values are stored as
|
|
162
|
+
ISO 8601 UTC text, `bool` as 0/1, `dict`/`list` as JSON text.
|
|
163
|
+
- The `gpkg_contents` extent grows on every write; `layer.update_extent()` recomputes it.
|
|
164
|
+
- RTree spatial index (`rtree_<t>_<c>`) with the standard triggers, registered in
|
|
165
|
+
`gpkg_extensions`. GeoPackage 1.3 trigger set for new files; the 1.4 set
|
|
166
|
+
(`update5`-`update7`) when extending a 1.4 file written by GDAL.
|
|
167
|
+
`ST_MinX/MaxX/MinY/MaxY`, `ST_IsEmpty` and `ST_SRID` are registered on every
|
|
168
|
+
connection so these triggers work from Python.
|
|
169
|
+
- Metadata extension: `gpkg.add_metadata(...)`, `gpkg.metadata()`.
|
|
170
|
+
- `gpkg.transaction()` groups several operations atomically.
|
|
171
|
+
|
|
172
|
+
### Validation
|
|
173
|
+
|
|
174
|
+
`gpkglite.validate(path)` returns a `ValidationReport` (`report.ok`, `report.errors`,
|
|
175
|
+
`report.warnings`, `report.as_dict()`), with requirement numbers from the specification:
|
|
176
|
+
|
|
177
|
+
- SQLite header, `application_id`, `user_version`, `integrity_check`, `foreign_key_check`;
|
|
178
|
+
- structure of `gpkg_spatial_ref_sys`, `gpkg_contents`, `gpkg_geometry_columns`,
|
|
179
|
+
`gpkg_extensions`, `gpkg_metadata`, `gpkg_metadata_reference`; mandatory SRS rows;
|
|
180
|
+
- references between contents, geometry columns and SRS; timestamps; column types;
|
|
181
|
+
integer primary keys;
|
|
182
|
+
- every geometry blob: header, envelope code, ExtendedGeoPackageBinary, SRS id, empty flag,
|
|
183
|
+
WKB, declared geometry type, Z/M constraints, envelope matching the coordinates;
|
|
184
|
+
- the RTree extension: registration, columns, triggers for the file's version, and that
|
|
185
|
+
the index holds exactly the non-empty geometries.
|
|
186
|
+
|
|
187
|
+
### Conversion helpers
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
gpkglite.from_geojson("roads.geojson", "out.gpkg") # layer "roads"
|
|
191
|
+
gpkglite.from_csv("stations.csv", "out.gpkg", x="lon", y="lat") # POINT layer
|
|
192
|
+
doc = gpkglite.to_geojson("out.gpkg", "roads", "roads.geojson")
|
|
193
|
+
text = gpkglite.to_csv("out.gpkg", "stations", geometry="xy")
|
|
194
|
+
|
|
195
|
+
# with the [geo] extra
|
|
196
|
+
gdf = gpkglite.to_geodataframe("out.gpkg", "roads")
|
|
197
|
+
gpkglite.from_geodataframe(gdf.to_crs(32635), "out.gpkg", "roads_utm")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`from_geojson` infers the schema and geometry type (mixing `Polygon` and `MultiPolygon`
|
|
201
|
+
gives a `MULTIPOLYGON` layer) and honours the legacy `crs` member.
|
|
202
|
+
|
|
203
|
+
### Command line
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
gpkglite info data.gpkg # layers, schema, CRS, extent, extensions (--json)
|
|
207
|
+
gpkglite dump data.gpkg --layer roads --format geojson
|
|
208
|
+
gpkglite dump data.gpkg -l roads -f csv --where "type = 'primary'" --bbox 29,41,29.1,41.1
|
|
209
|
+
gpkglite dump data.gpkg -l roads -f jsonl --limit 100 --offset 200 -o page3.jsonl
|
|
210
|
+
gpkglite validate data.gpkg # exit status 1 when invalid (--json)
|
|
211
|
+
gpkglite from-geojson in.geojson -o out.gpkg [--layer name] [--srs-id 3857]
|
|
212
|
+
gpkglite from-csv points.csv -o out.gpkg -x lon -y lat
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Interoperability
|
|
216
|
+
|
|
217
|
+
The test-suite cross-checks with GDAL:
|
|
218
|
+
|
|
219
|
+
- files written by gpkglite are read by `pyogrio` (GDAL 3.12 wheel) and, when available,
|
|
220
|
+
by `ogrinfo` and GDAL's `validate_gpkg.py`;
|
|
221
|
+
- the committed fixtures in `tests/data` were written by `ogr2ogr` (GDAL 3.13,
|
|
222
|
+
GeoPackage 1.4) and cover all geometry types, Z/M, empty and NULL geometries,
|
|
223
|
+
DATE/DATETIME/BOOLEAN fields and curves; gpkglite reads them and writes into them
|
|
224
|
+
through GDAL's 1.4 RTree triggers;
|
|
225
|
+
- QGIS 3 (OGR provider) loads every layer type gpkglite writes.
|
|
226
|
+
|
|
227
|
+
## Scope and limitations
|
|
228
|
+
|
|
229
|
+
- Vector features and attribute tables only: no tiles, gridded coverage, or related
|
|
230
|
+
tables extensions.
|
|
231
|
+
- Curve geometries are recognised but not decoded.
|
|
232
|
+
- No coordinate transformation: coordinates are stored in the layer CRS as given.
|
|
233
|
+
- `where` and `order_by` are inserted into SQL verbatim; never build them from untrusted
|
|
234
|
+
input. Bind values through `params`.
|
|
235
|
+
- SQLite must provide the R\*Tree module for spatial indexes (true for CPython on
|
|
236
|
+
Windows, macOS and mainstream Linux distributions, and for Pyodide).
|
|
237
|
+
|
|
238
|
+
## Development
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pip install -e ".[dev,geo]"
|
|
242
|
+
pytest --cov=gpkglite
|
|
243
|
+
ruff check . && ruff format --check src tests && mypy src
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Set `GPKGLITE_OGRINFO` (path to `ogrinfo`) and `GPKGLITE_VALIDATE_GPKG` (command that runs
|
|
247
|
+
GDAL's `validate_gpkg.py`) to enable the optional GDAL cross-validation tests.
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT, see [LICENSE](LICENSE). Citation metadata is in [CITATION.cff](CITATION.cff).
|
gpkglite-0.1.0/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# gpkglite
|
|
2
|
+
|
|
3
|
+
**Read, write and validate OGC GeoPackage files in pure Python. No GDAL, no compiled
|
|
4
|
+
dependencies, standard library only (`sqlite3`, `struct`, `json`).**
|
|
5
|
+
|
|
6
|
+
[](https://github.com/YusufEminoglu/gpkglite_sdk/actions/workflows/ci.yml)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
GeoPackage (`.gpkg`) is the OGC standard container for vector data: an SQLite database
|
|
11
|
+
with a few well-defined tables and a binary geometry encoding. Reading or writing one
|
|
12
|
+
usually means installing GDAL (via `fiona`, `pyogrio`, `geopandas` or `osgeo`). That is
|
|
13
|
+
heavy, or impossible, in:
|
|
14
|
+
|
|
15
|
+
- serverless functions (AWS Lambda, Cloud Functions) with tight package size limits,
|
|
16
|
+
- slim Docker images and CI jobs,
|
|
17
|
+
- Pyodide / JupyterLite in the browser,
|
|
18
|
+
- Windows machines without OSGeo4W or conda,
|
|
19
|
+
- libraries that want to accept a `.gpkg` without pulling GDAL into their dependency tree.
|
|
20
|
+
|
|
21
|
+
`gpkglite` is a small, typed, dependency-free implementation of the parts of
|
|
22
|
+
[OGC 12-128r18 (GeoPackage 1.3.1)](https://www.geopackage.org/spec131/) that vector
|
|
23
|
+
workflows need. Files it writes open in GDAL/OGR, QGIS, geopandas/pyogrio and fiona,
|
|
24
|
+
and it reads files written by them.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install gpkglite # standard library only
|
|
30
|
+
pip install "gpkglite[geo]" # + shapely / geopandas interop helpers
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import gpkglite
|
|
37
|
+
|
|
38
|
+
# write
|
|
39
|
+
with gpkglite.create("cities.gpkg") as gpkg:
|
|
40
|
+
layer = gpkg.create_layer(
|
|
41
|
+
"cities", "POINT", srs_id=4326,
|
|
42
|
+
fields={"name": "TEXT", "population": "INTEGER", "capital": "BOOLEAN"},
|
|
43
|
+
)
|
|
44
|
+
layer.insert({"type": "Point", "coordinates": [28.97, 41.01]},
|
|
45
|
+
{"name": "Istanbul", "population": 15_600_000, "capital": False})
|
|
46
|
+
layer.insert({"type": "Point", "coordinates": [32.85, 39.93]},
|
|
47
|
+
{"name": "Ankara", "population": 5_700_000, "capital": True})
|
|
48
|
+
|
|
49
|
+
# read
|
|
50
|
+
with gpkglite.open("cities.gpkg") as gpkg:
|
|
51
|
+
for info in gpkg.layers():
|
|
52
|
+
print(info.name, info.geometry_type, info.srs_id, info.bounds)
|
|
53
|
+
|
|
54
|
+
cities = gpkg["cities"]
|
|
55
|
+
print(cities.fields) # {'name': 'TEXT', 'population': 'INTEGER', 'capital': 'BOOLEAN'}
|
|
56
|
+
print(cities.crs.authority) # 'EPSG:4326'
|
|
57
|
+
for feature in cities.features(where="population > ?", params=[10_000_000]):
|
|
58
|
+
print(feature["properties"]["name"], feature["geometry"]["coordinates"])
|
|
59
|
+
|
|
60
|
+
# check it
|
|
61
|
+
report = gpkglite.validate("cities.gpkg")
|
|
62
|
+
print(report) # cities.gpkg: valid GeoPackage 1.3.1 (0 errors, ...)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
### Reading
|
|
68
|
+
|
|
69
|
+
- `gpkg.layers()`: every `gpkg_contents` entry (features and attribute tables) with
|
|
70
|
+
geometry column, geometry type, Z/M flags, SRS id and extent.
|
|
71
|
+
- `layer.columns`, `layer.fields`, `layer.schema`: column names and GeoPackage types.
|
|
72
|
+
- `gpkg.srs(id)`, `gpkg.list_srs()`, `layer.crs`: `gpkg_spatial_ref_sys` records,
|
|
73
|
+
including the WKT2 `definition_12_063` column when the CRS WKT extension is present.
|
|
74
|
+
- `layer.features(...)` yields GeoJSON-like `Feature` dicts; `layer.rows(...)` yields flat
|
|
75
|
+
dicts. Both accept:
|
|
76
|
+
- `where` + `params`: an SQL boolean expression with `?` or `:name` placeholders,
|
|
77
|
+
- `bbox=(minx, miny, maxx, maxy)`: uses the `rtree_<table>_<column>` index when present,
|
|
78
|
+
followed by an exact envelope test,
|
|
79
|
+
- `limit`, `offset`, `order_by`, `columns`,
|
|
80
|
+
- `geometry_format="geojson" | "wkb" | "gpkg" | "shapely"`,
|
|
81
|
+
- `on_error="raise" | "null" | "skip"` for geometries gpkglite cannot decode.
|
|
82
|
+
- `layer.count(...)`, `layer.get(fid)`, `layer.compute_bounds()`, `layer.to_geojson()`.
|
|
83
|
+
|
|
84
|
+
### Geometry encoding
|
|
85
|
+
|
|
86
|
+
- StandardGeoPackageBinary headers: magic, version, flags, both byte orders, all envelope
|
|
87
|
+
types (0 none, 1 XY, 2 XYZ, 3 XYM, 4 XYZM), the empty-geometry flag, SRS id.
|
|
88
|
+
- ISO WKB (and PostGIS extended WKB on input) for Point, LineString, Polygon, MultiPoint,
|
|
89
|
+
MultiLineString, MultiPolygon and GeometryCollection, in XY, XYZ, XYM and XYZM,
|
|
90
|
+
either byte order, nested collections, empty geometries (NaN points).
|
|
91
|
+
- Curve and surface types (CircularString, CompoundCurve, CurvePolygon, MultiCurve,
|
|
92
|
+
MultiSurface, PolyhedralSurface, TIN, Triangle) and ExtendedGeoPackageBinary raise
|
|
93
|
+
`UnsupportedGeometryError` with a readable message. Their raw WKB is still available
|
|
94
|
+
with `geometry_format="wkb"`, and their header envelope still drives `bbox` filters.
|
|
95
|
+
- Low-level API: `gpkglite.wkb.loads/dumps`, `gpkglite.encode_geometry`,
|
|
96
|
+
`gpkglite.decode_geometry`, `gpkglite.wkt.dumps`.
|
|
97
|
+
|
|
98
|
+
Geometries are plain dictionaries. Positions have 2, 3 or 4 numbers; a measured geometry
|
|
99
|
+
carries `"dims": "XYM"` (or `"XYZM"`) so it can be told apart from XYZ:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
{"type": "LineString", "coordinates": [[0, 0, 12.5], [10, 0, 13.0]]} # XYZ
|
|
103
|
+
{"type": "Point", "coordinates": [1, 2, 0.75], "dims": "XYM"} # XYM
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Any object with `__geo_interface__` (shapely, `geojson`, ...) is accepted as input.
|
|
107
|
+
|
|
108
|
+
### Writing
|
|
109
|
+
|
|
110
|
+
- `gpkglite.create(path)`: `application_id = 'GPKG'`, `user_version = 10301`, the required
|
|
111
|
+
`gpkg_spatial_ref_sys` rows (-1, 0, EPSG:4326), `gpkg_contents`,
|
|
112
|
+
`gpkg_geometry_columns`. `create(path, wkt2=True)` adds the CRS WKT extension.
|
|
113
|
+
- `gpkg.add_srs(...)`: built-in definitions for EPSG 4326, 3857, 4258, 4269 and all
|
|
114
|
+
WGS 84 / UTM zones (32601-32660, 32701-32760); any other CRS with an explicit WKT.
|
|
115
|
+
- `gpkg.create_layer(name, geometry_type, srs_id, fields, z=0|1|2, m=0|1|2,
|
|
116
|
+
spatial_index=True)` and `gpkg.create_table(name, fields)` for attribute-only tables.
|
|
117
|
+
Field types: `BOOLEAN, TINYINT, SMALLINT, MEDIUMINT, INT, INTEGER, FLOAT, DOUBLE, REAL,
|
|
118
|
+
TEXT, TEXT(n), BLOB, BLOB(n), DATE, DATETIME`, or Python types (`int`, `str`, `bool`,
|
|
119
|
+
`float`, `bytes`, `datetime.date`, `datetime.datetime`).
|
|
120
|
+
- `layer.insert(...)`, `layer.write_features(...)` (one transaction), `layer.update(...)`,
|
|
121
|
+
`layer.delete(fid)`, `layer.delete_where(...)`, `layer.add_field(...)`. Geometry type
|
|
122
|
+
and Z/M constraints of the layer are enforced; `date`/`datetime` values are stored as
|
|
123
|
+
ISO 8601 UTC text, `bool` as 0/1, `dict`/`list` as JSON text.
|
|
124
|
+
- The `gpkg_contents` extent grows on every write; `layer.update_extent()` recomputes it.
|
|
125
|
+
- RTree spatial index (`rtree_<t>_<c>`) with the standard triggers, registered in
|
|
126
|
+
`gpkg_extensions`. GeoPackage 1.3 trigger set for new files; the 1.4 set
|
|
127
|
+
(`update5`-`update7`) when extending a 1.4 file written by GDAL.
|
|
128
|
+
`ST_MinX/MaxX/MinY/MaxY`, `ST_IsEmpty` and `ST_SRID` are registered on every
|
|
129
|
+
connection so these triggers work from Python.
|
|
130
|
+
- Metadata extension: `gpkg.add_metadata(...)`, `gpkg.metadata()`.
|
|
131
|
+
- `gpkg.transaction()` groups several operations atomically.
|
|
132
|
+
|
|
133
|
+
### Validation
|
|
134
|
+
|
|
135
|
+
`gpkglite.validate(path)` returns a `ValidationReport` (`report.ok`, `report.errors`,
|
|
136
|
+
`report.warnings`, `report.as_dict()`), with requirement numbers from the specification:
|
|
137
|
+
|
|
138
|
+
- SQLite header, `application_id`, `user_version`, `integrity_check`, `foreign_key_check`;
|
|
139
|
+
- structure of `gpkg_spatial_ref_sys`, `gpkg_contents`, `gpkg_geometry_columns`,
|
|
140
|
+
`gpkg_extensions`, `gpkg_metadata`, `gpkg_metadata_reference`; mandatory SRS rows;
|
|
141
|
+
- references between contents, geometry columns and SRS; timestamps; column types;
|
|
142
|
+
integer primary keys;
|
|
143
|
+
- every geometry blob: header, envelope code, ExtendedGeoPackageBinary, SRS id, empty flag,
|
|
144
|
+
WKB, declared geometry type, Z/M constraints, envelope matching the coordinates;
|
|
145
|
+
- the RTree extension: registration, columns, triggers for the file's version, and that
|
|
146
|
+
the index holds exactly the non-empty geometries.
|
|
147
|
+
|
|
148
|
+
### Conversion helpers
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
gpkglite.from_geojson("roads.geojson", "out.gpkg") # layer "roads"
|
|
152
|
+
gpkglite.from_csv("stations.csv", "out.gpkg", x="lon", y="lat") # POINT layer
|
|
153
|
+
doc = gpkglite.to_geojson("out.gpkg", "roads", "roads.geojson")
|
|
154
|
+
text = gpkglite.to_csv("out.gpkg", "stations", geometry="xy")
|
|
155
|
+
|
|
156
|
+
# with the [geo] extra
|
|
157
|
+
gdf = gpkglite.to_geodataframe("out.gpkg", "roads")
|
|
158
|
+
gpkglite.from_geodataframe(gdf.to_crs(32635), "out.gpkg", "roads_utm")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`from_geojson` infers the schema and geometry type (mixing `Polygon` and `MultiPolygon`
|
|
162
|
+
gives a `MULTIPOLYGON` layer) and honours the legacy `crs` member.
|
|
163
|
+
|
|
164
|
+
### Command line
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
gpkglite info data.gpkg # layers, schema, CRS, extent, extensions (--json)
|
|
168
|
+
gpkglite dump data.gpkg --layer roads --format geojson
|
|
169
|
+
gpkglite dump data.gpkg -l roads -f csv --where "type = 'primary'" --bbox 29,41,29.1,41.1
|
|
170
|
+
gpkglite dump data.gpkg -l roads -f jsonl --limit 100 --offset 200 -o page3.jsonl
|
|
171
|
+
gpkglite validate data.gpkg # exit status 1 when invalid (--json)
|
|
172
|
+
gpkglite from-geojson in.geojson -o out.gpkg [--layer name] [--srs-id 3857]
|
|
173
|
+
gpkglite from-csv points.csv -o out.gpkg -x lon -y lat
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Interoperability
|
|
177
|
+
|
|
178
|
+
The test-suite cross-checks with GDAL:
|
|
179
|
+
|
|
180
|
+
- files written by gpkglite are read by `pyogrio` (GDAL 3.12 wheel) and, when available,
|
|
181
|
+
by `ogrinfo` and GDAL's `validate_gpkg.py`;
|
|
182
|
+
- the committed fixtures in `tests/data` were written by `ogr2ogr` (GDAL 3.13,
|
|
183
|
+
GeoPackage 1.4) and cover all geometry types, Z/M, empty and NULL geometries,
|
|
184
|
+
DATE/DATETIME/BOOLEAN fields and curves; gpkglite reads them and writes into them
|
|
185
|
+
through GDAL's 1.4 RTree triggers;
|
|
186
|
+
- QGIS 3 (OGR provider) loads every layer type gpkglite writes.
|
|
187
|
+
|
|
188
|
+
## Scope and limitations
|
|
189
|
+
|
|
190
|
+
- Vector features and attribute tables only: no tiles, gridded coverage, or related
|
|
191
|
+
tables extensions.
|
|
192
|
+
- Curve geometries are recognised but not decoded.
|
|
193
|
+
- No coordinate transformation: coordinates are stored in the layer CRS as given.
|
|
194
|
+
- `where` and `order_by` are inserted into SQL verbatim; never build them from untrusted
|
|
195
|
+
input. Bind values through `params`.
|
|
196
|
+
- SQLite must provide the R\*Tree module for spatial indexes (true for CPython on
|
|
197
|
+
Windows, macOS and mainstream Linux distributions, and for Pyodide).
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
pip install -e ".[dev,geo]"
|
|
203
|
+
pytest --cov=gpkglite
|
|
204
|
+
ruff check . && ruff format --check src tests && mypy src
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Set `GPKGLITE_OGRINFO` (path to `ogrinfo`) and `GPKGLITE_VALIDATE_GPKG` (command that runs
|
|
208
|
+
GDAL's `validate_gpkg.py`) to enable the optional GDAL cross-validation tests.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT, see [LICENSE](LICENSE). Citation metadata is in [CITATION.cff](CITATION.cff).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Create, query and validate a GeoPackage with gpkglite (standard library only).
|
|
2
|
+
|
|
3
|
+
Run: python examples/quickstart.py [output.gpkg]
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import gpkglite
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(path: str = "quickstart.gpkg") -> None:
|
|
14
|
+
with gpkglite.create(path, overwrite=True) as gpkg:
|
|
15
|
+
cities = gpkg.create_layer(
|
|
16
|
+
"cities",
|
|
17
|
+
"POINT",
|
|
18
|
+
srs_id=4326,
|
|
19
|
+
fields={"name": "TEXT", "population": "INTEGER", "capital": "BOOLEAN"},
|
|
20
|
+
)
|
|
21
|
+
cities.write_features(
|
|
22
|
+
[
|
|
23
|
+
(
|
|
24
|
+
{"type": "Point", "coordinates": [28.97, 41.01]},
|
|
25
|
+
{"name": "Istanbul", "population": 15_600_000, "capital": False},
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
{"type": "Point", "coordinates": [32.85, 39.93]},
|
|
29
|
+
{"name": "Ankara", "population": 5_700_000, "capital": True},
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
{"type": "Point", "coordinates": [27.14, 38.42]},
|
|
33
|
+
{"name": "Izmir", "population": 4_400_000, "capital": False},
|
|
34
|
+
),
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
roads = gpkg.create_layer("roads", "LINESTRING", srs_id=32635, fields={"ref": str}, z=1)
|
|
38
|
+
roads.insert(
|
|
39
|
+
{
|
|
40
|
+
"type": "LineString",
|
|
41
|
+
"coordinates": [[500000, 4500000, 10.0], [500250, 4500400, 42.5]],
|
|
42
|
+
},
|
|
43
|
+
{"ref": "D100"},
|
|
44
|
+
)
|
|
45
|
+
gpkg.add_metadata("<note>created by examples/quickstart.py</note>")
|
|
46
|
+
|
|
47
|
+
with gpkglite.open(path) as gpkg:
|
|
48
|
+
for info in gpkg.layers():
|
|
49
|
+
print(f"{info.name}: {info.geometry_type} srs_id={info.srs_id} bounds={info.bounds}")
|
|
50
|
+
big = gpkg["cities"].features(where="population > ?", params=[5_000_000])
|
|
51
|
+
print("big cities:", [f["properties"]["name"] for f in big])
|
|
52
|
+
west = gpkg["cities"].features(bbox=(26.0, 38.0, 30.0, 42.0))
|
|
53
|
+
print("west of 30E:", [f["properties"]["name"] for f in west])
|
|
54
|
+
|
|
55
|
+
print(gpkglite.validate(path))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main(*sys.argv[1:])
|