geocsv 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.
- geocsv-0.1.0/CHANGELOG.md +26 -0
- geocsv-0.1.0/CITATION.cff +20 -0
- geocsv-0.1.0/LICENSE +21 -0
- geocsv-0.1.0/MANIFEST.in +9 -0
- geocsv-0.1.0/PKG-INFO +233 -0
- geocsv-0.1.0/README.md +192 -0
- geocsv-0.1.0/examples/quickstart.py +35 -0
- geocsv-0.1.0/pyproject.toml +133 -0
- geocsv-0.1.0/setup.cfg +4 -0
- geocsv-0.1.0/src/geocsv/__init__.py +57 -0
- geocsv-0.1.0/src/geocsv/__main__.py +7 -0
- geocsv-0.1.0/src/geocsv/cli.py +202 -0
- geocsv-0.1.0/src/geocsv/detect.py +477 -0
- geocsv-0.1.0/src/geocsv/dms.py +260 -0
- geocsv-0.1.0/src/geocsv/errors.py +25 -0
- geocsv-0.1.0/src/geocsv/geohash.py +91 -0
- geocsv-0.1.0/src/geocsv/geometry.py +167 -0
- geocsv-0.1.0/src/geocsv/gpkg.py +229 -0
- geocsv-0.1.0/src/geocsv/maidenhead.py +75 -0
- geocsv-0.1.0/src/geocsv/names.py +165 -0
- geocsv-0.1.0/src/geocsv/parsers.py +177 -0
- geocsv-0.1.0/src/geocsv/pluscode.py +175 -0
- geocsv-0.1.0/src/geocsv/py.typed +0 -0
- geocsv-0.1.0/src/geocsv/shapefile_writer.py +112 -0
- geocsv-0.1.0/src/geocsv/table.py +911 -0
- geocsv-0.1.0/src/geocsv/textio.py +247 -0
- geocsv-0.1.0/src/geocsv/utm.py +305 -0
- geocsv-0.1.0/src/geocsv/wkt.py +347 -0
- geocsv-0.1.0/src/geocsv/writers.py +214 -0
- geocsv-0.1.0/src/geocsv.egg-info/PKG-INFO +233 -0
- geocsv-0.1.0/src/geocsv.egg-info/SOURCES.txt +61 -0
- geocsv-0.1.0/src/geocsv.egg-info/dependency_links.txt +1 -0
- geocsv-0.1.0/src/geocsv.egg-info/entry_points.txt +2 -0
- geocsv-0.1.0/src/geocsv.egg-info/requires.txt +12 -0
- geocsv-0.1.0/src/geocsv.egg-info/top_level.txt +1 -0
- geocsv-0.1.0/tests/conftest.py +47 -0
- geocsv-0.1.0/tests/data/dms_columns.csv +6 -0
- geocsv-0.1.0/tests/data/dms_pair.csv +6 -0
- geocsv-0.1.0/tests/data/excel_sep.csv +5 -0
- geocsv-0.1.0/tests/data/geohash.csv +6 -0
- geocsv-0.1.0/tests/data/geojson_column.csv +5 -0
- geocsv-0.1.0/tests/data/german.tsv +5 -0
- geocsv-0.1.0/tests/data/issues.csv +14 -0
- geocsv-0.1.0/tests/data/latlon_basic.csv +9 -0
- geocsv-0.1.0/tests/data/lonlat_pair.csv +5 -0
- geocsv-0.1.0/tests/data/maidenhead.csv +5 -0
- geocsv-0.1.0/tests/data/mgrs.csv +5 -0
- geocsv-0.1.0/tests/data/no_header.csv +4 -0
- geocsv-0.1.0/tests/data/pair_column.csv +6 -0
- geocsv-0.1.0/tests/data/pluscode.csv +5 -0
- geocsv-0.1.0/tests/data/projected_xy.csv +4 -0
- geocsv-0.1.0/tests/data/swapped_columns.csv +7 -0
- geocsv-0.1.0/tests/data/utm.csv +5 -0
- geocsv-0.1.0/tests/data/values_only.csv +5 -0
- geocsv-0.1.0/tests/data/wkt.csv +9 -0
- geocsv-0.1.0/tests/test_cli.py +164 -0
- geocsv-0.1.0/tests/test_decoders.py +286 -0
- geocsv-0.1.0/tests/test_detect.py +265 -0
- geocsv-0.1.0/tests/test_dms.py +120 -0
- geocsv-0.1.0/tests/test_table.py +314 -0
- geocsv-0.1.0/tests/test_textio.py +133 -0
- geocsv-0.1.0/tests/test_wkt.py +277 -0
- geocsv-0.1.0/tests/test_writers.py +325 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-08
|
|
8
|
+
### Added
|
|
9
|
+
- `geocsv.read()` / `read_string()` return a `GeoTable` with detected geometry, typed
|
|
10
|
+
attributes (integer, real, text) and per-row issues; `geocsv.detect()` returns the
|
|
11
|
+
detection alone.
|
|
12
|
+
- CSV handling with the standard library only: encoding detection (UTF-8, UTF-8 BOM,
|
|
13
|
+
UTF-16, cp1252, latin-1, cp1254), delimiter sniffing (`,` `;` tab `|`), Excel's
|
|
14
|
+
`sep=` line, decimal commas, quoted fields and header detection.
|
|
15
|
+
- Geometry detection with a confidence score and reasons: latitude/longitude columns by
|
|
16
|
+
header names in many languages or by value ranges, swapped-column detection, DMS
|
|
17
|
+
values, single-column coordinate pairs, WKT/EWKT, GeoJSON, geohash, Plus Codes
|
|
18
|
+
(including short codes with a reference point), Maidenhead locators, MGRS, UTM and
|
|
19
|
+
projected x/y with an EPSG code (`pyproj` extra).
|
|
20
|
+
- Pure-Python decoders: DMS parser and formatter, geohash, Open Location Code,
|
|
21
|
+
Maidenhead, UTM and MGRS (Krüger series, WGS 84).
|
|
22
|
+
- Validation: missing, invalid, empty, out-of-range, swapped and likely-swapped rows,
|
|
23
|
+
(0, 0) "null island", duplicates and ragged rows, with a text report and JSON summary.
|
|
24
|
+
- Writers: GeoJSON, GeoJSONSeq, KML, GPX and an OGC GeoPackage written with `sqlite3`
|
|
25
|
+
only; ESRI Shapefile through the `shapefile` extra.
|
|
26
|
+
- `geocsv` command with `inspect`, `detect` and `convert` sub-commands.
|
|
@@ -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: "geocsv"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-08"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/geocsv_sdk"
|
|
13
|
+
url: "https://pypi.org/project/geocsv/"
|
|
14
|
+
abstract: "Pure-Python reader that finds the coordinates in any CSV or spreadsheet export (lat/lon columns, DMS, WKT, GeoJSON, geohash, Plus Codes, Maidenhead, MGRS, UTM), validates them and writes GeoJSON, GeoJSONSeq, KML, GPX or GeoPackage without GDAL."
|
|
15
|
+
keywords:
|
|
16
|
+
- gis
|
|
17
|
+
- csv
|
|
18
|
+
- geojson
|
|
19
|
+
- geopackage
|
|
20
|
+
- coordinates
|
geocsv-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.
|
geocsv-0.1.0/MANIFEST.in
ADDED
geocsv-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geocsv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turn any spreadsheet or CSV of places into a map-ready GeoJSON, GeoPackage, KML or GPX file, without GDAL.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/geocsv_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/geocsv_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/geocsv_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/geocsv_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: gis,csv,geojson,geopackage,gpkg,kml,gpx,wkt,latitude,longitude,geocoding,dms,geohash,plus-codes,open-location-code,mgrs,utm,maidenhead,spreadsheet,data-cleaning
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Classifier: Topic :: Text Processing
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Classifier: Environment :: Console
|
|
24
|
+
Classifier: Intended Audience :: Science/Research
|
|
25
|
+
Classifier: Intended Audience :: Developers
|
|
26
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Provides-Extra: pyproj
|
|
32
|
+
Requires-Dist: pyproj>=3.3.0; extra == "pyproj"
|
|
33
|
+
Provides-Extra: shapefile
|
|
34
|
+
Requires-Dist: pyshp>=2.3.0; extra == "shapefile"
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
39
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
<div align="center">
|
|
43
|
+
|
|
44
|
+
# geocsv
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/geocsv/)
|
|
47
|
+
[](https://pypi.org/project/geocsv/)
|
|
48
|
+
[](https://github.com/YusufEminoglu/geocsv_sdk/blob/main/LICENSE)
|
|
49
|
+
[](https://docs.astral.sh/ruff/)
|
|
50
|
+
|
|
51
|
+
**Turn any spreadsheet or CSV of places into a map-ready GeoJSON, GeoPackage, KML or GPX file, without GDAL.**
|
|
52
|
+
|
|
53
|
+
[📦 PyPI](https://pypi.org/project/geocsv/) • [💻 GitHub](https://github.com/YusufEminoglu/geocsv_sdk) • [🐛 Issues](https://github.com/YusufEminoglu/geocsv_sdk/issues)
|
|
54
|
+
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
You have a spreadsheet export with a column called `Breitengrad`, or `enlem;boylam`
|
|
58
|
+
with decimal commas, or `40°26′46″N 79°58′56″W` strings, or Plus Codes, or MGRS
|
|
59
|
+
references. geocsv finds the coordinates, tells you how sure it is and why, reports the
|
|
60
|
+
rows that are broken, and writes a file that QGIS, ArcGIS, Google Earth, a GPS or a web
|
|
61
|
+
map can open. It is pure Python with **no required dependencies**.
|
|
62
|
+
|
|
63
|
+
## 📦 Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install geocsv
|
|
67
|
+
|
|
68
|
+
# optional extras
|
|
69
|
+
pip install "geocsv[pyproj]" # reproject x/y or EWKT from any EPSG code
|
|
70
|
+
pip install "geocsv[shapefile]" # write ESRI Shapefiles (via pyshp)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Python 3.9+ on Windows, macOS or Linux.
|
|
74
|
+
|
|
75
|
+
## ⚡ Quickstart
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
geocsv inspect places.csv # what was detected, and which rows are wrong
|
|
79
|
+
geocsv convert places.csv -o places.gpkg # GeoPackage (also .geojson .geojsonl .kml .gpx .shp)
|
|
80
|
+
geocsv convert places.csv -o places.geojson --lat "Y koord" --lon "X koord"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import geocsv
|
|
85
|
+
|
|
86
|
+
table = geocsv.read("places.csv")
|
|
87
|
+
print(table.detection.describe())
|
|
88
|
+
# latlon: lat='Breitengrad', lon='Längengrad', decimal (confidence 0.99)
|
|
89
|
+
print(table.report())
|
|
90
|
+
|
|
91
|
+
table.to_gpkg("places.gpkg")
|
|
92
|
+
table.to_geojson("places.geojson")
|
|
93
|
+
fc = table.to_geojson() # or get the FeatureCollection as a dict
|
|
94
|
+
table.to_kml("places.kml")
|
|
95
|
+
table.to_gpx("places.gpx")
|
|
96
|
+
|
|
97
|
+
geocsv.detect("places.csv") # detection only, with .reasons and .alternatives
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 🔎 What it reads
|
|
101
|
+
|
|
102
|
+
**CSV dialects** (standard library only)
|
|
103
|
+
|
|
104
|
+
* encodings: UTF-8, UTF-8 with BOM, UTF-16 (Excel "Unicode text"), and the Windows code
|
|
105
|
+
pages cp1252 / latin-1 / cp1254 (Turkish letters such as ğ ı ş are told apart from
|
|
106
|
+
Western European text)
|
|
107
|
+
* delimiters `,` `;` tab `|` (sniffed from the data, quoted fields respected) and Excel's
|
|
108
|
+
`sep=;` first line
|
|
109
|
+
* decimal commas (`40,7128`), header or no header (detected), ragged rows
|
|
110
|
+
|
|
111
|
+
**Geometry encodings**, found automatically:
|
|
112
|
+
|
|
113
|
+
| kind | example | notes |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `latlon` | `lat` / `lon` columns | header names in many languages: lat, latitude, latitud, Breitengrad, enlem, широта, lng, long, Längengrad, boylam, долгота, … plus value ranges when names are missing |
|
|
116
|
+
| `latlon` (DMS) | `40°26′46″N`, `N40 26.767`, `40d26m46s` | degrees, minutes, seconds, any symbols or letters |
|
|
117
|
+
| `pair` | `"41.0082, 28.9784"`, `40°26′46″N 79°58′56″W` | one column; lat/lon order inferred from ranges, hemisphere letters or a `lonlat` header |
|
|
118
|
+
| `wkt` | `POINT (29 41)`, `SRID=3857;POLYGON ((…))` | all simple-feature types, Z/M/ZM, EWKT |
|
|
119
|
+
| `geojson` | `{"type": "Point", "coordinates": [29, 41]}` | geometry or Feature objects |
|
|
120
|
+
| `geohash` | `sxk97cn4` | |
|
|
121
|
+
| `pluscode` | `8GFJWVM5+9V` | Open Location Code; short codes with `plus_code_reference` |
|
|
122
|
+
| `maidenhead` | `KM69kw` | 2-10 characters |
|
|
123
|
+
| `mgrs` | `35T PF 66370 41552` | WGS 84, pure-Python Krüger series |
|
|
124
|
+
| `utm` | `35T 666371 4541552` | zone + band or hemisphere |
|
|
125
|
+
| `xy` | `easting` / `northing` columns | any EPSG code via the `pyproj` extra |
|
|
126
|
+
|
|
127
|
+
Every detection has a confidence and human-readable reasons:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
$ geocsv detect stations.csv
|
|
131
|
+
latlon: lat='lon', lon='lat', decimal (confidence 0.99)
|
|
132
|
+
- header 'lat' looks like latitude but its values only fit longitude; the columns are swapped
|
|
133
|
+
- 100% of values are coordinates; 100% are within latitude/longitude range
|
|
134
|
+
- the two columns are adjacent
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Override any decision: `lat=`/`lon=`, `wkt=`, `column=` + `kind=`, `x=`/`y=` + `epsg=`,
|
|
138
|
+
`order="lonlat"`, `encoding=`, `delimiter=`, `header=` (CLI: `--lat`, `--lon`, `--wkt`,
|
|
139
|
+
`--column`, `--kind`, `--x`, `--y`, `--epsg`, `--order`, `--encoding`, `--delimiter`,
|
|
140
|
+
`--header/--no-header`). Columns can be named or numbered from 1.
|
|
141
|
+
|
|
142
|
+
## 🩺 Validation report
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
$ geocsv inspect issues.csv
|
|
146
|
+
Geometry latlon: lat='latitude', lon='longitude', decimal (confidence 0.87)
|
|
147
|
+
Valid 8 of 13 rows bbox=(0.000000, 0.000000, 37.874600, 41.008200)
|
|
148
|
+
|
|
149
|
+
issue severity rows lines
|
|
150
|
+
-------------- -------- ----- --------------------
|
|
151
|
+
missing error 2 3, 4
|
|
152
|
+
invalid error 1 5
|
|
153
|
+
out_of_range error 1 6
|
|
154
|
+
swapped error 1 9
|
|
155
|
+
likely_swapped warning 1 13
|
|
156
|
+
null_island warning 1 7
|
|
157
|
+
duplicate warning 1 8
|
|
158
|
+
ragged warning 1 14
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
| code | severity | meaning |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `missing` | error | empty geometry cell(s) |
|
|
164
|
+
| `invalid` | error | the value does not parse |
|
|
165
|
+
| `empty` | error | WKT/GeoJSON `EMPTY` geometry |
|
|
166
|
+
| `out_of_range` | error | outside ±90 / ±180 |
|
|
167
|
+
| `swapped` | error | latitude beyond ±90 but would fit if swapped (`fix_swapped=True` repairs it) |
|
|
168
|
+
| `crs` | error | EWKT in another CRS and `pyproj` is not installed |
|
|
169
|
+
| `likely_swapped` | warning | in range, but far closer to the rest of the table when swapped |
|
|
170
|
+
| `swapped_fixed` | warning | swapped back (by `fix_swapped=True` or hemisphere letters) |
|
|
171
|
+
| `null_island` | warning | exactly (0, 0) |
|
|
172
|
+
| `duplicate` | warning | same geometry as an earlier row |
|
|
173
|
+
| `ragged` | warning | row length differs from the header |
|
|
174
|
+
|
|
175
|
+
Line numbers are physical lines in the file, as your editor shows them.
|
|
176
|
+
`table.issues`, `table.summary()` and `geocsv inspect --json` give the same data in
|
|
177
|
+
machine-readable form; `geocsv convert --strict` exits with status 1 when any row has an
|
|
178
|
+
error.
|
|
179
|
+
|
|
180
|
+
## 🗺️ What it writes
|
|
181
|
+
|
|
182
|
+
| format | method | notes |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| GeoJSON | `to_geojson(path=None)` | RFC 7946 FeatureCollection; returns a dict without a path |
|
|
185
|
+
| GeoJSONSeq | `to_geojsonseq(path)` | one Feature per line (`.geojsonl`, `.ndjson`) |
|
|
186
|
+
| GeoPackage | `to_gpkg(path, layer=None)` | pure `sqlite3`; OGC GeoPackage 1.4 tables, StandardGeoPackageBinary + ISO WKB, any geometry type, Z |
|
|
187
|
+
| KML | `to_kml(path=None)` | Placemarks with ExtendedData, holes, MultiGeometry |
|
|
188
|
+
| GPX | `to_gpx(path=None)` | points as waypoints, lines as tracks |
|
|
189
|
+
| Shapefile | `to_shapefile(path)` | needs `geocsv[shapefile]`; one geometry family |
|
|
190
|
+
| any | `to_file(path, driver=None)` | picks the format from the extension |
|
|
191
|
+
|
|
192
|
+
Attributes keep their types: integer, real or text (codes like `"00123"` stay text).
|
|
193
|
+
A `name`-like column (`name`, `title`, `isim`, `nombre`, …) labels KML placemarks and GPX
|
|
194
|
+
waypoints. The GeoPackage writer is tested against `sqlite3`, GDAL's `ogrinfo`, GDAL's
|
|
195
|
+
`validate_gpkg.py` conformance checker and GeoPandas.
|
|
196
|
+
|
|
197
|
+
## 🧭 Coordinate helpers
|
|
198
|
+
|
|
199
|
+
The decoders are public and dependency-free:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from geocsv import dms, geohash, pluscode, maidenhead, utm
|
|
203
|
+
|
|
204
|
+
dms.parse_dms_pair("40°26′46″N 79°58′56″W") # (40.4461, -79.9822)
|
|
205
|
+
dms.format_dms(40.446111, "lat") # '40°26′46.0″N'
|
|
206
|
+
geohash.decode("u4pruydqqvj") # (57.64911, 10.40744)
|
|
207
|
+
pluscode.decode("849VCWC8+R9") # (37.4220625, -122.0840625)
|
|
208
|
+
pluscode.recover_nearest("CWC8+R9", 37.4, -122.0)
|
|
209
|
+
maidenhead.decode("FN31pr") # (41.7292, -72.7083)
|
|
210
|
+
utm.mgrs_to_latlon("31U DQ 48251 11932") # (48.8582, 2.2945)
|
|
211
|
+
utm.latlon_to_mgrs(48.8582, 2.2945) # '31U DQ 48251 11932'
|
|
212
|
+
utm.utm_to_latlon(31, "N", 448251, 5411932)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
UTM/MGRS use Krüger's series to sixth order; they agree with PROJ to under a millimetre.
|
|
216
|
+
|
|
217
|
+
## 🛠️ Development
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
git clone https://github.com/YusufEminoglu/geocsv_sdk.git
|
|
221
|
+
cd geocsv_sdk
|
|
222
|
+
pip install -e ".[dev,pyproj,shapefile]"
|
|
223
|
+
python -m pytest --cov=geocsv
|
|
224
|
+
python -m ruff check . && python -m ruff format --check src tests && python -m mypy src
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The tests include hand-made fixture CSVs, reference values for every decoder, a seeded
|
|
228
|
+
synthetic corpus of 400 tables that measures detection accuracy, and GDAL-based checks
|
|
229
|
+
of the GeoPackage output that run when GDAL is installed.
|
|
230
|
+
|
|
231
|
+
## 📄 License
|
|
232
|
+
|
|
233
|
+
MIT © Yusuf Eminoğlu
|
geocsv-0.1.0/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# geocsv
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/geocsv/)
|
|
6
|
+
[](https://pypi.org/project/geocsv/)
|
|
7
|
+
[](https://github.com/YusufEminoglu/geocsv_sdk/blob/main/LICENSE)
|
|
8
|
+
[](https://docs.astral.sh/ruff/)
|
|
9
|
+
|
|
10
|
+
**Turn any spreadsheet or CSV of places into a map-ready GeoJSON, GeoPackage, KML or GPX file, without GDAL.**
|
|
11
|
+
|
|
12
|
+
[📦 PyPI](https://pypi.org/project/geocsv/) • [💻 GitHub](https://github.com/YusufEminoglu/geocsv_sdk) • [🐛 Issues](https://github.com/YusufEminoglu/geocsv_sdk/issues)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
You have a spreadsheet export with a column called `Breitengrad`, or `enlem;boylam`
|
|
17
|
+
with decimal commas, or `40°26′46″N 79°58′56″W` strings, or Plus Codes, or MGRS
|
|
18
|
+
references. geocsv finds the coordinates, tells you how sure it is and why, reports the
|
|
19
|
+
rows that are broken, and writes a file that QGIS, ArcGIS, Google Earth, a GPS or a web
|
|
20
|
+
map can open. It is pure Python with **no required dependencies**.
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install geocsv
|
|
26
|
+
|
|
27
|
+
# optional extras
|
|
28
|
+
pip install "geocsv[pyproj]" # reproject x/y or EWKT from any EPSG code
|
|
29
|
+
pip install "geocsv[shapefile]" # write ESRI Shapefiles (via pyshp)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Python 3.9+ on Windows, macOS or Linux.
|
|
33
|
+
|
|
34
|
+
## ⚡ Quickstart
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
geocsv inspect places.csv # what was detected, and which rows are wrong
|
|
38
|
+
geocsv convert places.csv -o places.gpkg # GeoPackage (also .geojson .geojsonl .kml .gpx .shp)
|
|
39
|
+
geocsv convert places.csv -o places.geojson --lat "Y koord" --lon "X koord"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import geocsv
|
|
44
|
+
|
|
45
|
+
table = geocsv.read("places.csv")
|
|
46
|
+
print(table.detection.describe())
|
|
47
|
+
# latlon: lat='Breitengrad', lon='Längengrad', decimal (confidence 0.99)
|
|
48
|
+
print(table.report())
|
|
49
|
+
|
|
50
|
+
table.to_gpkg("places.gpkg")
|
|
51
|
+
table.to_geojson("places.geojson")
|
|
52
|
+
fc = table.to_geojson() # or get the FeatureCollection as a dict
|
|
53
|
+
table.to_kml("places.kml")
|
|
54
|
+
table.to_gpx("places.gpx")
|
|
55
|
+
|
|
56
|
+
geocsv.detect("places.csv") # detection only, with .reasons and .alternatives
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 🔎 What it reads
|
|
60
|
+
|
|
61
|
+
**CSV dialects** (standard library only)
|
|
62
|
+
|
|
63
|
+
* encodings: UTF-8, UTF-8 with BOM, UTF-16 (Excel "Unicode text"), and the Windows code
|
|
64
|
+
pages cp1252 / latin-1 / cp1254 (Turkish letters such as ğ ı ş are told apart from
|
|
65
|
+
Western European text)
|
|
66
|
+
* delimiters `,` `;` tab `|` (sniffed from the data, quoted fields respected) and Excel's
|
|
67
|
+
`sep=;` first line
|
|
68
|
+
* decimal commas (`40,7128`), header or no header (detected), ragged rows
|
|
69
|
+
|
|
70
|
+
**Geometry encodings**, found automatically:
|
|
71
|
+
|
|
72
|
+
| kind | example | notes |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `latlon` | `lat` / `lon` columns | header names in many languages: lat, latitude, latitud, Breitengrad, enlem, широта, lng, long, Längengrad, boylam, долгота, … plus value ranges when names are missing |
|
|
75
|
+
| `latlon` (DMS) | `40°26′46″N`, `N40 26.767`, `40d26m46s` | degrees, minutes, seconds, any symbols or letters |
|
|
76
|
+
| `pair` | `"41.0082, 28.9784"`, `40°26′46″N 79°58′56″W` | one column; lat/lon order inferred from ranges, hemisphere letters or a `lonlat` header |
|
|
77
|
+
| `wkt` | `POINT (29 41)`, `SRID=3857;POLYGON ((…))` | all simple-feature types, Z/M/ZM, EWKT |
|
|
78
|
+
| `geojson` | `{"type": "Point", "coordinates": [29, 41]}` | geometry or Feature objects |
|
|
79
|
+
| `geohash` | `sxk97cn4` | |
|
|
80
|
+
| `pluscode` | `8GFJWVM5+9V` | Open Location Code; short codes with `plus_code_reference` |
|
|
81
|
+
| `maidenhead` | `KM69kw` | 2-10 characters |
|
|
82
|
+
| `mgrs` | `35T PF 66370 41552` | WGS 84, pure-Python Krüger series |
|
|
83
|
+
| `utm` | `35T 666371 4541552` | zone + band or hemisphere |
|
|
84
|
+
| `xy` | `easting` / `northing` columns | any EPSG code via the `pyproj` extra |
|
|
85
|
+
|
|
86
|
+
Every detection has a confidence and human-readable reasons:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
$ geocsv detect stations.csv
|
|
90
|
+
latlon: lat='lon', lon='lat', decimal (confidence 0.99)
|
|
91
|
+
- header 'lat' looks like latitude but its values only fit longitude; the columns are swapped
|
|
92
|
+
- 100% of values are coordinates; 100% are within latitude/longitude range
|
|
93
|
+
- the two columns are adjacent
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Override any decision: `lat=`/`lon=`, `wkt=`, `column=` + `kind=`, `x=`/`y=` + `epsg=`,
|
|
97
|
+
`order="lonlat"`, `encoding=`, `delimiter=`, `header=` (CLI: `--lat`, `--lon`, `--wkt`,
|
|
98
|
+
`--column`, `--kind`, `--x`, `--y`, `--epsg`, `--order`, `--encoding`, `--delimiter`,
|
|
99
|
+
`--header/--no-header`). Columns can be named or numbered from 1.
|
|
100
|
+
|
|
101
|
+
## 🩺 Validation report
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
$ geocsv inspect issues.csv
|
|
105
|
+
Geometry latlon: lat='latitude', lon='longitude', decimal (confidence 0.87)
|
|
106
|
+
Valid 8 of 13 rows bbox=(0.000000, 0.000000, 37.874600, 41.008200)
|
|
107
|
+
|
|
108
|
+
issue severity rows lines
|
|
109
|
+
-------------- -------- ----- --------------------
|
|
110
|
+
missing error 2 3, 4
|
|
111
|
+
invalid error 1 5
|
|
112
|
+
out_of_range error 1 6
|
|
113
|
+
swapped error 1 9
|
|
114
|
+
likely_swapped warning 1 13
|
|
115
|
+
null_island warning 1 7
|
|
116
|
+
duplicate warning 1 8
|
|
117
|
+
ragged warning 1 14
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| code | severity | meaning |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `missing` | error | empty geometry cell(s) |
|
|
123
|
+
| `invalid` | error | the value does not parse |
|
|
124
|
+
| `empty` | error | WKT/GeoJSON `EMPTY` geometry |
|
|
125
|
+
| `out_of_range` | error | outside ±90 / ±180 |
|
|
126
|
+
| `swapped` | error | latitude beyond ±90 but would fit if swapped (`fix_swapped=True` repairs it) |
|
|
127
|
+
| `crs` | error | EWKT in another CRS and `pyproj` is not installed |
|
|
128
|
+
| `likely_swapped` | warning | in range, but far closer to the rest of the table when swapped |
|
|
129
|
+
| `swapped_fixed` | warning | swapped back (by `fix_swapped=True` or hemisphere letters) |
|
|
130
|
+
| `null_island` | warning | exactly (0, 0) |
|
|
131
|
+
| `duplicate` | warning | same geometry as an earlier row |
|
|
132
|
+
| `ragged` | warning | row length differs from the header |
|
|
133
|
+
|
|
134
|
+
Line numbers are physical lines in the file, as your editor shows them.
|
|
135
|
+
`table.issues`, `table.summary()` and `geocsv inspect --json` give the same data in
|
|
136
|
+
machine-readable form; `geocsv convert --strict` exits with status 1 when any row has an
|
|
137
|
+
error.
|
|
138
|
+
|
|
139
|
+
## 🗺️ What it writes
|
|
140
|
+
|
|
141
|
+
| format | method | notes |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| GeoJSON | `to_geojson(path=None)` | RFC 7946 FeatureCollection; returns a dict without a path |
|
|
144
|
+
| GeoJSONSeq | `to_geojsonseq(path)` | one Feature per line (`.geojsonl`, `.ndjson`) |
|
|
145
|
+
| GeoPackage | `to_gpkg(path, layer=None)` | pure `sqlite3`; OGC GeoPackage 1.4 tables, StandardGeoPackageBinary + ISO WKB, any geometry type, Z |
|
|
146
|
+
| KML | `to_kml(path=None)` | Placemarks with ExtendedData, holes, MultiGeometry |
|
|
147
|
+
| GPX | `to_gpx(path=None)` | points as waypoints, lines as tracks |
|
|
148
|
+
| Shapefile | `to_shapefile(path)` | needs `geocsv[shapefile]`; one geometry family |
|
|
149
|
+
| any | `to_file(path, driver=None)` | picks the format from the extension |
|
|
150
|
+
|
|
151
|
+
Attributes keep their types: integer, real or text (codes like `"00123"` stay text).
|
|
152
|
+
A `name`-like column (`name`, `title`, `isim`, `nombre`, …) labels KML placemarks and GPX
|
|
153
|
+
waypoints. The GeoPackage writer is tested against `sqlite3`, GDAL's `ogrinfo`, GDAL's
|
|
154
|
+
`validate_gpkg.py` conformance checker and GeoPandas.
|
|
155
|
+
|
|
156
|
+
## 🧭 Coordinate helpers
|
|
157
|
+
|
|
158
|
+
The decoders are public and dependency-free:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from geocsv import dms, geohash, pluscode, maidenhead, utm
|
|
162
|
+
|
|
163
|
+
dms.parse_dms_pair("40°26′46″N 79°58′56″W") # (40.4461, -79.9822)
|
|
164
|
+
dms.format_dms(40.446111, "lat") # '40°26′46.0″N'
|
|
165
|
+
geohash.decode("u4pruydqqvj") # (57.64911, 10.40744)
|
|
166
|
+
pluscode.decode("849VCWC8+R9") # (37.4220625, -122.0840625)
|
|
167
|
+
pluscode.recover_nearest("CWC8+R9", 37.4, -122.0)
|
|
168
|
+
maidenhead.decode("FN31pr") # (41.7292, -72.7083)
|
|
169
|
+
utm.mgrs_to_latlon("31U DQ 48251 11932") # (48.8582, 2.2945)
|
|
170
|
+
utm.latlon_to_mgrs(48.8582, 2.2945) # '31U DQ 48251 11932'
|
|
171
|
+
utm.utm_to_latlon(31, "N", 448251, 5411932)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
UTM/MGRS use Krüger's series to sixth order; they agree with PROJ to under a millimetre.
|
|
175
|
+
|
|
176
|
+
## 🛠️ Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
git clone https://github.com/YusufEminoglu/geocsv_sdk.git
|
|
180
|
+
cd geocsv_sdk
|
|
181
|
+
pip install -e ".[dev,pyproj,shapefile]"
|
|
182
|
+
python -m pytest --cov=geocsv
|
|
183
|
+
python -m ruff check . && python -m ruff format --check src tests && python -m mypy src
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The tests include hand-made fixture CSVs, reference values for every decoder, a seeded
|
|
187
|
+
synthetic corpus of 400 tables that measures detection accuracy, and GDAL-based checks
|
|
188
|
+
of the GeoPackage output that run when GDAL is installed.
|
|
189
|
+
|
|
190
|
+
## 📄 License
|
|
191
|
+
|
|
192
|
+
MIT © Yusuf Eminoğlu
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Read a few CSV layouts, print the detection report and write a GeoPackage.
|
|
2
|
+
|
|
3
|
+
Run from a clone: python examples/quickstart.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import geocsv
|
|
12
|
+
|
|
13
|
+
SAMPLES = {
|
|
14
|
+
"turkish_decimal_comma.csv": "isim;enlem;boylam\nAnkara;39,9334;32,8597\nİzmir;38,4237;27,1428\n",
|
|
15
|
+
"dms.csv": 'site,position\nPittsburgh,"40°26′46″N 79°58′56″W"\nOslo,"59°54′50″N 10°45′08″E"\n',
|
|
16
|
+
"wkt.csv": 'name,wkt\nPark,"POLYGON ((29 41, 29.01 41, 29.01 41.01, 29 41))"\n',
|
|
17
|
+
"codes.csv": "shop,plus_code\nGoogleplex,849VCWC8+R9\nAnkara,8GFJWVM5+9V\n",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
23
|
+
folder = Path(tmp)
|
|
24
|
+
for name, text in SAMPLES.items():
|
|
25
|
+
path = folder / name
|
|
26
|
+
path.write_text(text, encoding="utf-8")
|
|
27
|
+
table = geocsv.read(path)
|
|
28
|
+
print(table.report())
|
|
29
|
+
print()
|
|
30
|
+
out = table.to_gpkg(path.with_suffix(".gpkg"))
|
|
31
|
+
print(f"wrote {out.name} with {len(table.valid)} features\n")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "geocsv"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Yusuf Eminoğlu", email = "yusufeminoglu@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
description = "Turn any spreadsheet or CSV of places into a map-ready GeoJSON, GeoPackage, KML or GPX file, without GDAL."
|
|
12
|
+
keywords = [
|
|
13
|
+
"gis",
|
|
14
|
+
"csv",
|
|
15
|
+
"geojson",
|
|
16
|
+
"geopackage",
|
|
17
|
+
"gpkg",
|
|
18
|
+
"kml",
|
|
19
|
+
"gpx",
|
|
20
|
+
"wkt",
|
|
21
|
+
"latitude",
|
|
22
|
+
"longitude",
|
|
23
|
+
"geocoding",
|
|
24
|
+
"dms",
|
|
25
|
+
"geohash",
|
|
26
|
+
"plus-codes",
|
|
27
|
+
"open-location-code",
|
|
28
|
+
"mgrs",
|
|
29
|
+
"utm",
|
|
30
|
+
"maidenhead",
|
|
31
|
+
"spreadsheet",
|
|
32
|
+
"data-cleaning",
|
|
33
|
+
]
|
|
34
|
+
readme = "README.md"
|
|
35
|
+
license = "MIT"
|
|
36
|
+
license-files = ["LICENSE"]
|
|
37
|
+
requires-python = ">=3.9"
|
|
38
|
+
classifiers = [
|
|
39
|
+
"Development Status :: 4 - Beta",
|
|
40
|
+
"Programming Language :: Python :: 3",
|
|
41
|
+
"Programming Language :: Python :: 3.9",
|
|
42
|
+
"Programming Language :: Python :: 3.10",
|
|
43
|
+
"Programming Language :: Python :: 3.11",
|
|
44
|
+
"Programming Language :: Python :: 3.12",
|
|
45
|
+
"Programming Language :: Python :: 3.13",
|
|
46
|
+
"Operating System :: OS Independent",
|
|
47
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
48
|
+
"Topic :: Text Processing",
|
|
49
|
+
"Topic :: Utilities",
|
|
50
|
+
"Environment :: Console",
|
|
51
|
+
"Intended Audience :: Science/Research",
|
|
52
|
+
"Intended Audience :: Developers",
|
|
53
|
+
"Intended Audience :: End Users/Desktop",
|
|
54
|
+
"Typing :: Typed",
|
|
55
|
+
]
|
|
56
|
+
dependencies = []
|
|
57
|
+
|
|
58
|
+
[project.optional-dependencies]
|
|
59
|
+
pyproj = [
|
|
60
|
+
"pyproj>=3.3.0",
|
|
61
|
+
]
|
|
62
|
+
shapefile = [
|
|
63
|
+
"pyshp>=2.3.0",
|
|
64
|
+
]
|
|
65
|
+
dev = [
|
|
66
|
+
"pytest>=7.0.0",
|
|
67
|
+
"pytest-cov>=4.0.0",
|
|
68
|
+
"ruff>=0.1.0",
|
|
69
|
+
"mypy>=1.0.0",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[project.scripts]
|
|
73
|
+
geocsv = "geocsv.cli:main"
|
|
74
|
+
|
|
75
|
+
[project.urls]
|
|
76
|
+
Homepage = "https://github.com/YusufEminoglu/geocsv_sdk"
|
|
77
|
+
Repository = "https://github.com/YusufEminoglu/geocsv_sdk"
|
|
78
|
+
Issues = "https://github.com/YusufEminoglu/geocsv_sdk/issues"
|
|
79
|
+
Changelog = "https://github.com/YusufEminoglu/geocsv_sdk/blob/main/CHANGELOG.md"
|
|
80
|
+
|
|
81
|
+
[tool.setuptools]
|
|
82
|
+
package-dir = {"" = "src"}
|
|
83
|
+
|
|
84
|
+
[tool.setuptools.packages.find]
|
|
85
|
+
where = ["src"]
|
|
86
|
+
|
|
87
|
+
[tool.setuptools.package-data]
|
|
88
|
+
geocsv = ["py.typed"]
|
|
89
|
+
|
|
90
|
+
[tool.ruff]
|
|
91
|
+
line-length = 100
|
|
92
|
+
target-version = "py39"
|
|
93
|
+
exclude = [
|
|
94
|
+
"scratch",
|
|
95
|
+
".venv",
|
|
96
|
+
"build",
|
|
97
|
+
"dist",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint]
|
|
101
|
+
select = ["E", "F", "W", "I", "B", "C4"]
|
|
102
|
+
ignore = ["E501"]
|
|
103
|
+
|
|
104
|
+
[tool.mypy]
|
|
105
|
+
files = ["src"]
|
|
106
|
+
warn_unused_ignores = true
|
|
107
|
+
warn_redundant_casts = true
|
|
108
|
+
|
|
109
|
+
[[tool.mypy.overrides]]
|
|
110
|
+
module = [
|
|
111
|
+
# Optional extras: every import of them is lazy and guarded.
|
|
112
|
+
"pyproj.*",
|
|
113
|
+
"shapefile.*",
|
|
114
|
+
]
|
|
115
|
+
ignore_missing_imports = true
|
|
116
|
+
|
|
117
|
+
[tool.pytest.ini_options]
|
|
118
|
+
testpaths = ["tests"]
|
|
119
|
+
# Lets `pytest` run straight from a clone, without an editable install first.
|
|
120
|
+
pythonpath = ["src"]
|
|
121
|
+
|
|
122
|
+
[tool.coverage.run]
|
|
123
|
+
source = ["geocsv"]
|
|
124
|
+
branch = true
|
|
125
|
+
|
|
126
|
+
[tool.coverage.report]
|
|
127
|
+
show_missing = true
|
|
128
|
+
fail_under = 85
|
|
129
|
+
exclude_lines = [
|
|
130
|
+
"pragma: no cover",
|
|
131
|
+
"if TYPE_CHECKING:",
|
|
132
|
+
"raise NotImplementedError",
|
|
133
|
+
]
|
geocsv-0.1.0/setup.cfg
ADDED