ucrs 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.
ucrs-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Panos Mavrogiorgos
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.
ucrs-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.3
2
+ Name: ucrs
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Panos Mavrogiorgos
6
+ Author-email: pmav99@gmail.com
7
+ Requires-Python: >=3.10
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Provides-Extra: cartopy
14
+ Provides-Extra: dev
15
+ Provides-Extra: full
16
+ Provides-Extra: gdal
17
+ Provides-Extra: test
18
+ Requires-Dist: cartopy ; extra == "cartopy"
19
+ Requires-Dist: cartopy ; extra == "full"
20
+ Requires-Dist: gdal ; extra == "full"
21
+ Requires-Dist: gdal ; extra == "gdal"
22
+ Requires-Dist: pyproj
23
+ Requires-Dist: pyright (>=1.1.0) ; extra == "dev"
24
+ Requires-Dist: pytest (>=8.0.0) ; extra == "dev"
25
+ Requires-Dist: pytest (>=8.0.0) ; extra == "test"
26
+ Requires-Dist: pytest-cov (>=4.1.0) ; extra == "dev"
27
+ Requires-Dist: pytest-cov (>=4.1.0) ; extra == "test"
28
+ Requires-Dist: pytest-xdist (>=3.5.0) ; extra == "dev"
29
+ Requires-Dist: pytest-xdist (>=3.5.0) ; extra == "test"
30
+ Description-Content-Type: text/markdown
31
+
32
+ # UCRS - Unified CRS
33
+
34
+ Working with geospatial data in Python often means juggling multiple libraries, each with its own CRS representation:
35
+ - **pyproj** uses `pyproj.CRS`
36
+ - **cartopy** uses `cartopy.crs.CRS` and `cartopy.crs.Projection`
37
+ - **GDAL/osgeo** uses `osgeo.osr.SpatialReference`
38
+
39
+ Converting between these formats is well
40
+ [documented](https://pyproj4.github.io/pyproj/stable/crs_compatibility.html),
41
+ but a bit tedious.
42
+
43
+ UCRS solves this by providing a single wrapper class that accepts any CRS input
44
+ and lazily converts to an instance of any library you need.
45
+
46
+ ## Usage
47
+
48
+ ```python
49
+ from ucrs import UCRS
50
+
51
+ # Create UCRS from any CRS representation
52
+ ucrs = UCRS(4326) # EPSG code
53
+ ucrs = UCRS("EPSG:4326") # EPSG string
54
+ ucrs = UCRS(pyproj.CRS.from_epsg(4326)) # pyproj.CRS
55
+ ucrs = UCRS(cartopy.crs.PlateCarree()) # cartopy CRS
56
+ ucrs = UCRS(srs) # osgeo.osr.SpatialReference
57
+
58
+ # Access different representations
59
+ ucrs.proj # Always available
60
+ ucrs.cartopy # Requires cartopy
61
+ ucrs.osgeo # Requires GDAL
62
+ ```
63
+
64
+ ## How It Works
65
+
66
+ UCRS uses `pyproj.CRS` as its internal canonical representation. All conversions follow this pattern:
67
+
68
+ 1. **Input** → `pyproj.CRS` (during initialization)
69
+ 2. `pyproj.CRS` → **Output format** (via cached properties)
70
+
71
+ Conversions are lazy and cached, so accessing `.proj`, `.cartopy`, or `.osgeo` multiple times returns the same object without re-conversion.
72
+
73
+ ## Requirements
74
+
75
+ - Python 3.10+
76
+ - pyproj (required)
77
+ - cartopy (optional)
78
+ - GDAL (optional)
79
+
80
+ ## Installation
81
+
82
+ ```bash
83
+ # Minimal installation (pyproj only)
84
+ pip install ucrs
85
+
86
+ # With cartopy support
87
+ pip install ucrs[cartopy]
88
+
89
+ # With GDAL support
90
+ pip install ucrs[gdal]
91
+
92
+ # With all optional dependencies
93
+ pip install ucrs[full]
94
+ ```
95
+
96
+
97
+ ## License
98
+
99
+ MIT License - see LICENSE file for details.
100
+
ucrs-0.1.0/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # UCRS - Unified CRS
2
+
3
+ Working with geospatial data in Python often means juggling multiple libraries, each with its own CRS representation:
4
+ - **pyproj** uses `pyproj.CRS`
5
+ - **cartopy** uses `cartopy.crs.CRS` and `cartopy.crs.Projection`
6
+ - **GDAL/osgeo** uses `osgeo.osr.SpatialReference`
7
+
8
+ Converting between these formats is well
9
+ [documented](https://pyproj4.github.io/pyproj/stable/crs_compatibility.html),
10
+ but a bit tedious.
11
+
12
+ UCRS solves this by providing a single wrapper class that accepts any CRS input
13
+ and lazily converts to an instance of any library you need.
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from ucrs import UCRS
19
+
20
+ # Create UCRS from any CRS representation
21
+ ucrs = UCRS(4326) # EPSG code
22
+ ucrs = UCRS("EPSG:4326") # EPSG string
23
+ ucrs = UCRS(pyproj.CRS.from_epsg(4326)) # pyproj.CRS
24
+ ucrs = UCRS(cartopy.crs.PlateCarree()) # cartopy CRS
25
+ ucrs = UCRS(srs) # osgeo.osr.SpatialReference
26
+
27
+ # Access different representations
28
+ ucrs.proj # Always available
29
+ ucrs.cartopy # Requires cartopy
30
+ ucrs.osgeo # Requires GDAL
31
+ ```
32
+
33
+ ## How It Works
34
+
35
+ UCRS uses `pyproj.CRS` as its internal canonical representation. All conversions follow this pattern:
36
+
37
+ 1. **Input** → `pyproj.CRS` (during initialization)
38
+ 2. `pyproj.CRS` → **Output format** (via cached properties)
39
+
40
+ Conversions are lazy and cached, so accessing `.proj`, `.cartopy`, or `.osgeo` multiple times returns the same object without re-conversion.
41
+
42
+ ## Requirements
43
+
44
+ - Python 3.10+
45
+ - pyproj (required)
46
+ - cartopy (optional)
47
+ - GDAL (optional)
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ # Minimal installation (pyproj only)
53
+ pip install ucrs
54
+
55
+ # With cartopy support
56
+ pip install ucrs[cartopy]
57
+
58
+ # With GDAL support
59
+ pip install ucrs[gdal]
60
+
61
+ # With all optional dependencies
62
+ pip install ucrs[full]
63
+ ```
64
+
65
+
66
+ ## License
67
+
68
+ MIT License - see LICENSE file for details.
@@ -0,0 +1,75 @@
1
+ [project]
2
+ name = "ucrs"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = [
6
+ {name = "Panos Mavrogiorgos",email = "pmav99@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "pyproj"
12
+ ]
13
+
14
+ [project.optional-dependencies]
15
+ cartopy = ["cartopy"]
16
+ gdal = ["gdal"]
17
+ full = ["cartopy", "gdal"]
18
+ test = [
19
+ "pytest>=8.0.0",
20
+ "pytest-cov>=4.1.0",
21
+ "pytest-xdist>=3.5.0",
22
+ ]
23
+ dev = [
24
+ "pytest>=8.0.0",
25
+ "pytest-cov>=4.1.0",
26
+ "pytest-xdist>=3.5.0",
27
+ "pyright>=1.1.0",
28
+ ]
29
+
30
+ [build-system]
31
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
32
+ build-backend = "poetry.core.masonry.api"
33
+
34
+ [tool.poetry.group.dev.dependencies]
35
+ pytest = "*"
36
+ pytest-cov = "*"
37
+
38
+ [tool.pytest.ini_options]
39
+ minversion = "8.0"
40
+ addopts = [
41
+ "-ra",
42
+ "--strict-markers",
43
+ "--strict-config",
44
+ "--showlocals",
45
+ ]
46
+ testpaths = ["tests"]
47
+ pythonpath = ["."]
48
+ markers = [
49
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
50
+ "requires_cartopy: marks tests that require cartopy to be installed",
51
+ "requires_osgeo: marks tests that require osgeo/GDAL to be installed",
52
+ ]
53
+
54
+ [tool.coverage.run]
55
+ source = ["ucrs"]
56
+ branch = true
57
+ parallel = true
58
+
59
+ [tool.coverage.report]
60
+ precision = 2
61
+ show_missing = true
62
+ skip_covered = false
63
+ exclude_lines = [
64
+ "pragma: no cover",
65
+ "def __repr__",
66
+ "raise AssertionError",
67
+ "raise NotImplementedError",
68
+ "if __name__ == .__main__.:",
69
+ "if TYPE_CHECKING:",
70
+ "@abstractmethod",
71
+ ]
72
+
73
+ [tool.coverage.paths]
74
+ source = ["ucrs.py"]
75
+
ucrs-0.1.0/ucrs.py ADDED
@@ -0,0 +1,162 @@
1
+
2
+ """Unified CRS (UCRS) class for seamless conversion between pyproj, cartopy, and osgeo."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from functools import cached_property
7
+ from typing import cast
8
+ from typing import TypeAlias
9
+ from typing import TYPE_CHECKING
10
+
11
+ import pyproj
12
+
13
+ try:
14
+ import cartopy.crs as ccrs
15
+ CARTOPY_AVAILABLE = True
16
+ except ImportError:
17
+ CARTOPY_AVAILABLE = False # pyright: ignore[reportConstantRedefinition]
18
+
19
+ try:
20
+ import osgeo
21
+ from osgeo.osr import SpatialReference
22
+ OSGEO_AVAILABLE = True
23
+ except ImportError:
24
+ OSGEO_AVAILABLE = False # pyright: ignore[reportConstantRedefinition]
25
+
26
+ # Type alias for accepted input types
27
+ if TYPE_CHECKING:
28
+ import cartopy.crs as ccrs
29
+ from osgeo.osr import SpatialReference
30
+
31
+ CRSInput: TypeAlias = (
32
+ pyproj.CRS
33
+ | ccrs.CRS
34
+ | ccrs.Projection
35
+ | SpatialReference
36
+ | str
37
+ | int
38
+ | dict[str, str]
39
+ )
40
+
41
+
42
+ class UCRS:
43
+ """Unified CRS for seamless conversion between pyproj, cartopy, and osgeo.
44
+
45
+ Parameters
46
+ ----------
47
+ obj : CRSInput
48
+ Can be anything that pyproj.crs.CRS.from_user_input() accepts, or
49
+ instances of cartopy.crs.CRS, cartopy.crs.Projection, pyproj.CRS,
50
+ or osgeo.osr.SpatialReference.
51
+
52
+ Examples
53
+ --------
54
+ >>> ucrs = UCRS(4326)
55
+ >>> ucrs = UCRS("EPSG:4326")
56
+ >>> ucrs = UCRS(pyproj.CRS.from_epsg(4326))
57
+
58
+ Access different CRS representations:
59
+ >>> proj_crs = ucrs.proj
60
+ >>> cart_crs = ucrs.cartopy # if cartopy available
61
+ >>> osgeo_crs = ucrs.osgeo # if osgeo available
62
+ """
63
+
64
+ def __init__(self, obj: CRSInput) -> None:
65
+ """Initialize UCRS from various CRS representations."""
66
+ self._original: CRSInput = obj
67
+
68
+ # Convert input to pyproj.CRS
69
+ match obj:
70
+ case pyproj.CRS():
71
+ self._pyproj_crs: pyproj.CRS = obj
72
+
73
+ case _ if OSGEO_AVAILABLE and isinstance(obj, SpatialReference):
74
+ # Convert from osgeo to pyproj
75
+ wkt: str
76
+ if osgeo.version_info.major < 3:
77
+ wkt = cast(str, obj.ExportToWkt())
78
+ else:
79
+ wkt = cast(str, obj.ExportToWkt(["FORMAT=WKT2_2018"]))
80
+ self._pyproj_crs = pyproj.CRS.from_wkt(wkt)
81
+
82
+ case _ if CARTOPY_AVAILABLE and isinstance(obj, (ccrs.CRS, ccrs.Projection)):
83
+ # cartopy.crs.CRS inherits from pyproj.CRS, so use from_user_input
84
+ self._pyproj_crs = pyproj.CRS.from_user_input(obj)
85
+
86
+ case _:
87
+ # Handle all other inputs via from_user_input (str, int, dict, etc.)
88
+ self._pyproj_crs = pyproj.CRS.from_user_input(obj)
89
+
90
+ @cached_property
91
+ def proj(self) -> pyproj.CRS:
92
+ """Return pyproj.CRS representation."""
93
+ return self._pyproj_crs
94
+
95
+ @cached_property
96
+ def cartopy(self) -> ccrs.CRS | ccrs.Projection:
97
+ """Return cartopy.crs.CRS or cartopy.crs.Projection representation.
98
+
99
+ Returns
100
+ -------
101
+ ccrs.CRS or ccrs.Projection
102
+ Geographic CRS returns ccrs.CRS, projected CRS returns ccrs.Projection.
103
+
104
+ Raises
105
+ ------
106
+ ImportError
107
+ If cartopy is not installed.
108
+ RuntimeError
109
+ If the CRS cannot be converted (e.g., WKT2 not available).
110
+ """
111
+ if not CARTOPY_AVAILABLE:
112
+ raise ImportError(
113
+ "cartopy is not installed. Install it with: pip install cartopy"
114
+ )
115
+
116
+ proj_crs = self.proj
117
+
118
+ # Check if geographic or projected
119
+ if proj_crs.is_geographic:
120
+ return ccrs.CRS(proj_crs)
121
+ else:
122
+ return ccrs.Projection(proj_crs)
123
+
124
+ @cached_property
125
+ def osgeo(self) -> SpatialReference:
126
+ """Return osgeo.osr.SpatialReference representation.
127
+
128
+ Returns
129
+ -------
130
+ SpatialReference
131
+ GDAL/OGR Spatial Reference object.
132
+
133
+ Raises
134
+ ------
135
+ ImportError
136
+ If osgeo is not installed.
137
+ """
138
+ if not OSGEO_AVAILABLE:
139
+ raise ImportError(
140
+ "osgeo (GDAL) is not installed. Install it with: pip install gdal"
141
+ )
142
+
143
+ from pyproj.enums import WktVersion
144
+
145
+ proj_crs = self.proj
146
+ osr_crs = SpatialReference()
147
+
148
+ # Convert from pyproj to osgeo
149
+ if osgeo.version_info.major < 3:
150
+ osr_crs.ImportFromWkt(proj_crs.to_wkt(WktVersion.WKT1_GDAL))
151
+ else:
152
+ osr_crs.ImportFromWkt(proj_crs.to_wkt())
153
+
154
+ return osr_crs
155
+
156
+ def __repr__(self) -> str: # pyright: ignore[reportImplicitOverride]
157
+ """Return string representation."""
158
+ return f"UCRS({self.proj.name})"
159
+
160
+ def __str__(self) -> str: # pyright: ignore[reportImplicitOverride]
161
+ """Return string representation."""
162
+ return str(self.proj)