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.
Files changed (42) hide show
  1. gpkglite-0.1.0/CHANGELOG.md +35 -0
  2. gpkglite-0.1.0/CITATION.cff +20 -0
  3. gpkglite-0.1.0/LICENSE +21 -0
  4. gpkglite-0.1.0/MANIFEST.in +10 -0
  5. gpkglite-0.1.0/PKG-INFO +251 -0
  6. gpkglite-0.1.0/README.md +212 -0
  7. gpkglite-0.1.0/examples/quickstart.py +59 -0
  8. gpkglite-0.1.0/pyproject.toml +127 -0
  9. gpkglite-0.1.0/setup.cfg +4 -0
  10. gpkglite-0.1.0/src/gpkglite/__init__.py +79 -0
  11. gpkglite-0.1.0/src/gpkglite/__main__.py +7 -0
  12. gpkglite-0.1.0/src/gpkglite/_sql.py +187 -0
  13. gpkglite-0.1.0/src/gpkglite/binary.py +219 -0
  14. gpkglite-0.1.0/src/gpkglite/cli.py +210 -0
  15. gpkglite-0.1.0/src/gpkglite/convert.py +659 -0
  16. gpkglite-0.1.0/src/gpkglite/crs.py +155 -0
  17. gpkglite-0.1.0/src/gpkglite/errors.py +39 -0
  18. gpkglite-0.1.0/src/gpkglite/geopackage.py +1604 -0
  19. gpkglite-0.1.0/src/gpkglite/py.typed +0 -0
  20. gpkglite-0.1.0/src/gpkglite/validate.py +673 -0
  21. gpkglite-0.1.0/src/gpkglite/wkb.py +437 -0
  22. gpkglite-0.1.0/src/gpkglite/wkt.py +58 -0
  23. gpkglite-0.1.0/src/gpkglite.egg-info/PKG-INFO +251 -0
  24. gpkglite-0.1.0/src/gpkglite.egg-info/SOURCES.txt +40 -0
  25. gpkglite-0.1.0/src/gpkglite.egg-info/dependency_links.txt +1 -0
  26. gpkglite-0.1.0/src/gpkglite.egg-info/entry_points.txt +2 -0
  27. gpkglite-0.1.0/src/gpkglite.egg-info/requires.txt +10 -0
  28. gpkglite-0.1.0/src/gpkglite.egg-info/top_level.txt +1 -0
  29. gpkglite-0.1.0/tests/__init__.py +0 -0
  30. gpkglite-0.1.0/tests/conftest.py +107 -0
  31. gpkglite-0.1.0/tests/data/README.md +14 -0
  32. gpkglite-0.1.0/tests/data/gdal_curves.gpkg +0 -0
  33. gpkglite-0.1.0/tests/data/gdal_vectors.gpkg +0 -0
  34. gpkglite-0.1.0/tests/test_binary.py +130 -0
  35. gpkglite-0.1.0/tests/test_cli.py +184 -0
  36. gpkglite-0.1.0/tests/test_convert.py +275 -0
  37. gpkglite-0.1.0/tests/test_gdal_fixtures.py +136 -0
  38. gpkglite-0.1.0/tests/test_geopackage.py +524 -0
  39. gpkglite-0.1.0/tests/test_interop.py +154 -0
  40. gpkglite-0.1.0/tests/test_rtree.py +132 -0
  41. gpkglite-0.1.0/tests/test_validate.py +287 -0
  42. 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
@@ -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
+ [![CI](https://github.com/YusufEminoglu/gpkglite_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/gpkglite_sdk/actions/workflows/ci.yml)
46
+ ![Python](https://img.shields.io/badge/python-3.9%20%7C%203.11%20%7C%203.13-blue)
47
+ ![License](https://img.shields.io/badge/license-MIT-green)
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).
@@ -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
+ [![CI](https://github.com/YusufEminoglu/gpkglite_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/gpkglite_sdk/actions/workflows/ci.yml)
7
+ ![Python](https://img.shields.io/badge/python-3.9%20%7C%203.11%20%7C%203.13-blue)
8
+ ![License](https://img.shields.io/badge/license-MIT-green)
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:])