Skip to content

Commit 9d894ae

Browse files
committed
update SQL Directory env and collection Extent
1 parent c9df8fe commit 9d894ae

8 files changed

Lines changed: 139 additions & 107 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ repos:
2626
- toml
2727

2828
- repo: https://github.com/pre-commit/mirrors-mypy
29-
rev: v0.812
29+
rev: v0.991
3030
hooks:
3131
- id: mypy
3232
language_version: python
33+
# No reason to run if only tests have changed. They intentionally break typing.
34+
exclude: tests/.*

‎tests/conftest.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ def app(database_url, monkeypatch):
7070
monkeypatch.setenv("TIPG_TABLE_CONFIG__public_my_data_alt__pk", "id")
7171
monkeypatch.setenv("TIPG_TABLE_CONFIG__public_landsat__geomcol", "geom")
7272

73-
# monkeypatch.setenv("TIPG_FUNCTIONS_DIRECTORY", os.path.join(DATA_DIR, "functions"))
73+
# monkeypatch.setenv("TIPG_CUSTOM_SQL_DIRECTORY", os.path.join(DATA_DIR, "functions"))
7474

7575
# OGC Tiles Settings
7676
monkeypatch.setenv("TIPG_DEFAULT_MINZOOM", str(5))
7777
monkeypatch.setenv("TIPG_DEFAULT_MAXZOOM", str(12))
78-
monkeypatch.setenv("TIPG_FUNCTIONS_DIRECTORY", "tests/fixtures/functions")
78+
monkeypatch.setenv("TIPG_CUSTOM_SQL_DIRECTORY", "tests/fixtures/functions")
7979

8080
from tipg.main import app, postgres_settings
8181

‎tests/routes/test_collections.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def test_collections_search(app):
5151

5252
response = app.get("/collections", params={"bbox": "-180,81,180,87"})
5353
body = response.json()
54-
assert body["numberMatched"] == 9
54+
assert body["numberMatched"] == 10
5555
ids = [x["id"] for x in body["collections"]]
5656
assert "public.nongeo_data" not in ids
5757
assert "public.canada" not in ids
@@ -74,18 +74,17 @@ def test_collections_search(app):
7474

7575
response = app.get("/collections", params={"datetime": "2004-12-31T23:59:59Z/.."})
7676
body = response.json()
77-
assert body["numberMatched"] == 1
77+
assert body["numberMatched"] == 2
7878
ids = [x["id"] for x in body["collections"]]
79-
80-
assert ["public.my_data_alt"] == ids
79+
assert ["public.my_data", "public.my_data_alt"] == ids
8180

8281
response = app.get(
8382
"/collections", params={"datetime": "2004-01-01T00:00:00Z/2004-12-31T23:59:59Z"}
8483
)
8584
body = response.json()
86-
assert body["numberMatched"] == 2
85+
assert body["numberMatched"] == 1
8786
ids = [x["id"] for x in body["collections"]]
88-
assert ["public.my_data", "public.nongeo_data"] == ids
87+
assert ["public.nongeo_data"] == ids
8988

9089

9190
def test_collections_excludes(app_excludes):

‎tipg/db.py‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,36 @@
11
"""tipg.db: database events."""
22

3-
from pathlib import Path
3+
import os
4+
import pathlib
45
from typing import Any, Optional
56

67
import orjson
78
from buildpg import asyncpg
89

910
from tipg.dbmodel import get_collection_index
11+
from tipg.errors import FunctionDirectoryDoesNotExist
1012
from tipg.settings import PostgresSettings
1113

1214
from fastapi import FastAPI
1315

16+
custom_sql: list[pathlib.Path] = []
17+
user_sql_dir = os.environ.get("TIPG_CUSTOM_SQL_DIRECTORY", None)
18+
if user_sql_dir:
19+
if not pathlib.Path(user_sql_dir).exists():
20+
raise FunctionDirectoryDoesNotExist
21+
22+
custom_sql = list(pathlib.Path(user_sql_dir).glob("*.sql"))
1423

15-
async def con_init(
16-
conn,
17-
settings: Optional[PostgresSettings] = None,
18-
):
19-
"""Use json for json returns."""
20-
if not settings:
21-
settings = PostgresSettings()
2224

25+
async def con_init(conn):
26+
"""Use orjson encoder/decoder and register custom SQL functions."""
2327
await conn.set_type_codec(
2428
"json", encoder=orjson.dumps, decoder=orjson.loads, schema="pg_catalog"
2529
)
2630
await conn.set_type_codec(
2731
"jsonb", encoder=orjson.dumps, decoder=orjson.loads, schema="pg_catalog"
2832
)
33+
2934
await conn.execute(
3035
"""
3136
SELECT set_config(
@@ -35,13 +40,14 @@ async def con_init(
3540
);
3641
"""
3742
)
38-
if (
39-
settings.tipg_functions_directory
40-
and Path(settings.tipg_functions_directory).exists()
41-
):
42-
for sqlfile in Path(settings.tipg_functions_directory).glob("**/*.sql"):
43+
44+
# Register custom SQL functions/table/views
45+
if custom_sql:
46+
for sqlfile in custom_sql:
4347
await conn.execute(sqlfile.read_text())
44-
dbcatalogsql = Path("tipg/sql/dbcatalog.sql")
48+
49+
# Register TiPG functions
50+
dbcatalogsql = pathlib.Path(__package__) / "sql" / "dbcatalog.sql"
4551
await conn.execute(dbcatalogsql.read_text())
4652

4753

‎tipg/dbmodel.py‎

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
)
2323
from tipg.filter.evaluate import to_filter
2424
from tipg.filter.filters import bbox_to_wkt
25-
from tipg.model import Extent, Spatial, Temporal
25+
from tipg.model import Extent
2626
from tipg.settings import TableSettings, TileSettings
2727

2828
tile_settings = TileSettings()
@@ -135,9 +135,7 @@ def json_type(self) -> str:
135135
@property
136136
def is_geometry(self) -> bool:
137137
"""Returns true if this property is a geometry column."""
138-
if self.geometry_type:
139-
return True
140-
return False
138+
return self.type in ("geometry", "geography")
141139

142140
@property
143141
def is_datetime(self) -> bool:
@@ -170,26 +168,63 @@ class Collection(BaseModel):
170168
default_tms: str = tile_settings.default_tms
171169

172170
@property
173-
def geometry_columns(self):
174-
"""Return geometry columns."""
175-
return [c for c in self.properties if c.is_geometry]
171+
def extent(self) -> Optional[Extent]:
172+
"""Return extent."""
173+
extent = {}
174+
if cols := self.geometry_columns:
175+
if len(cols) == 1:
176+
bbox = [cols[0].bounds]
177+
else:
178+
minx, miny, maxx, maxy = zip(*[col.bounds for col in cols])
179+
bbox = [
180+
[min(minx), min(miny), max(maxx), max(maxy)],
181+
*[col.bounds for col in cols],
182+
]
183+
184+
extent["spatial"] = {
185+
"bbox": bbox,
186+
"crs": f"http://www.opengis.net/def/crs/EPSG/0/{cols[0].srid}",
187+
}
188+
189+
if cols := self.datetime_columns:
190+
cols = [col for col in cols if col.mindt or col.maxdt]
191+
192+
intervals = []
193+
if len(cols) == 1:
194+
if cols[0].mindt or cols[0].maxdt:
195+
intervals = [[cols[0].mindt, cols[0].maxdt]]
196+
197+
else:
198+
mindt = [col.mindt for col in cols if col.mindt]
199+
maxdt = [col.maxdt for col in cols if col.maxdt]
200+
intervals = [
201+
[min(mindt), max(maxdt)],
202+
*[[col.mindt, col.maxdt] for col in cols if col.mindt or col.maxdt],
203+
]
204+
205+
if intervals:
206+
extent["temporal"] = {"interval": intervals}
207+
208+
if extent:
209+
return Extent(**extent)
210+
211+
return None
176212

177213
@property
178-
def extent(self):
179-
"""Return extent."""
180-
spatial = None
181-
temporal = None
182-
if self.geometry_column and self.geometry_column.bounds:
183-
spatial = Spatial(bbox=[self.geometry_column.bounds], crs=self.crs)
184-
if (
185-
self.datetime_column
186-
and self.datetime_column.mindt
187-
and self.datetime_column.maxdt
188-
):
189-
temporal = Temporal(
190-
interval=[[self.datetime_column.mindt, self.datetime_column.maxdt]]
191-
)
192-
return Extent(spatial=spatial, temporal=temporal)
214+
def bounds(self) -> Optional[List[float]]:
215+
"""Return spatial bounds from collection extent."""
216+
if self.extent and self.extent.spatial:
217+
return self.extent.spatial.bbox[0]
218+
219+
return None
220+
221+
@property
222+
def dt_bounds(self) -> Optional[List[str]]:
223+
"""Return temporal bounds from collection extent."""
224+
if self.extent and self.extent.temporal:
225+
return self.extent.temporal.interval[0]
226+
227+
return None
193228

194229
@property
195230
def crs(self):
@@ -198,46 +233,43 @@ def crs(self):
198233
return f"http://www.opengis.net/def/crs/EPSG/0/{self.geometry_column.srid}"
199234

200235
@property
201-
def datetime_columns(self):
236+
def geometry_columns(self) -> List[Column]:
237+
"""Return geometry columns."""
238+
return [c for c in self.properties if c.is_geometry]
239+
240+
@property
241+
def datetime_columns(self) -> List[Column]:
202242
"""Return datetime columns."""
203243
return [c for c in self.properties if c.is_datetime]
204244

205-
def get_datetime_column(self, name: Optional[str] = None) -> Optional[Column]:
206-
"""Return the Column for either the passed in tstz column or the first tstz column."""
207-
if not self.datetime_columns:
245+
def get_geometry_column(self, name: Optional[str] = None) -> Optional[Column]:
246+
"""Return the name of the first geometry column."""
247+
if (not self.geometry_columns) or (name and name.lower() == "none"):
208248
return None
209249

210250
if name is None:
211-
return self.datetime_column
251+
return self.geometry_column
212252

213-
for col in self.datetime_columns:
253+
for col in self.geometry_columns:
214254
if col.name == name:
215255
return col
216256

217257
return None
218258

219-
def get_geometry_column(self, name: Optional[str] = None) -> Optional[Column]:
220-
"""Return the name of the first geometry column."""
221-
if (not self.geometry_columns) or (name and name.lower() == "none"):
259+
def get_datetime_column(self, name: Optional[str] = None) -> Optional[Column]:
260+
"""Return the Column for either the passed in tstz column or the first tstz column."""
261+
if not self.datetime_columns:
222262
return None
223263

224264
if name is None:
225-
return self.geometry_column
265+
return self.datetime_column
226266

227-
for col in self.geometry_columns:
267+
for col in self.datetime_columns:
228268
if col.name == name:
229269
return col
230270

231271
return None
232272

233-
@property
234-
def bounds(self) -> Optional[List[float]]:
235-
"""Return bounds from collection extent."""
236-
if self.extent and self.extent.spatial:
237-
return self.extent.spatial.bbox[0]
238-
239-
return None
240-
241273
@property
242274
def id_column_info(self) -> Column: # type: ignore
243275
"""Return Column for a unique identifier."""
@@ -913,6 +945,7 @@ async def get_collection_index( # noqa: C901
913945
or datetime_column is None
914946
):
915947
datetime_column = c
948+
916949
if c.get("type") in ("geometry", "geography"):
917950
if (
918951
table_conf.get("geomcol") == c["name"]

‎tipg/factory.py‎

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,17 @@ def write(self, line: str):
8787
yield writer.writerow(row)
8888

8989

90-
def s_intersects(bbox: List[float], spatial_extent: List[List[float]]) -> bool:
90+
def s_intersects(bbox: List[float], spatial_extent: List[float]) -> bool:
9191
"""Check if bbox intersects with spatial extent."""
92-
for ext in spatial_extent:
93-
if (
94-
(bbox[0] < ext[2])
95-
and (bbox[2] > ext[0])
96-
and (bbox[3] > ext[1])
97-
and (bbox[1] < ext[3])
98-
):
99-
return True
100-
return False
92+
return (
93+
(bbox[0] < spatial_extent[2])
94+
and (bbox[2] > spatial_extent[0])
95+
and (bbox[3] > spatial_extent[1])
96+
and (bbox[1] < spatial_extent[3])
97+
)
10198

10299

103-
def t_intersects(interval: List[str], temporal_extent: List[List[str]]) -> bool:
100+
def t_intersects(interval: List[str], temporal_extent: List[str]) -> bool:
104101
"""Check if dates intersect with temporal extent."""
105102
if len(interval) == 1:
106103
start = end = parse_rfc3339(interval[0])
@@ -109,22 +106,22 @@ def t_intersects(interval: List[str], temporal_extent: List[List[str]]) -> bool:
109106
start = parse_rfc3339(interval[0]) if not interval[0] in ["..", ""] else None
110107
end = parse_rfc3339(interval[1]) if not interval[1] in ["..", ""] else None
111108

112-
for (mint, maxt) in temporal_extent:
113-
min_ext = parse_rfc3339(mint) if mint is not None else None
114-
max_ext = parse_rfc3339(maxt) if maxt is not None else None
109+
mint, maxt = temporal_extent
110+
min_ext = parse_rfc3339(mint) if mint is not None else None
111+
max_ext = parse_rfc3339(maxt) if maxt is not None else None
115112

116-
if len(interval) == 1:
117-
if start == min_ext or start == max_ext:
118-
return True
113+
if len(interval) == 1:
114+
if start == min_ext or start == max_ext:
115+
return True
119116

120-
if not start:
121-
return max_ext <= end or min_ext <= end
117+
if not start:
118+
return max_ext <= end or min_ext <= end
122119

123-
elif not end:
124-
return min_ext >= start or max_ext >= start
120+
elif not end:
121+
return min_ext >= start or max_ext >= start
125122

126-
else:
127-
return min_ext >= start and max_ext <= end
123+
else:
124+
return min_ext >= start and max_ext <= end
128125

129126
return False
130127

@@ -447,21 +444,17 @@ def collections(
447444
collections_list = [
448445
collection
449446
for collection in collections_list
450-
if collection.extent is not None
451-
and collection.extent.spatial is not None
452-
and s_intersects(bbox_filter, collection.extent.spatial.bbox)
447+
if collection.bounds is not None
448+
and s_intersects(bbox_filter, collection.bounds)
453449
]
454450

455451
# datetime filter
456452
if datetime_filter is not None:
457453
collections_list = [
458454
collection
459455
for collection in collections_list
460-
if collection.extent is not None
461-
and collection.extent.temporal is not None
462-
and t_intersects(
463-
datetime_filter, collection.extent.temporal.interval
464-
)
456+
if collection.dt_bounds is not None
457+
and t_intersects(datetime_filter, collection.dt_bounds)
465458
]
466459

467460
matched_items = len(collections_list)

0 commit comments

Comments
 (0)