Skip to content

Commit aee7883

Browse files
authored
Merge branch 'main' into mapproxy
2 parents d70006b + 0c92a6f commit aee7883

14 files changed

Lines changed: 2245 additions & 2079 deletions

.pre-commit-config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,12 @@ repos:
6767
language: system
6868
types: [python]
6969
pass_filenames: false
70+
71+
- repo: local
72+
hooks:
73+
- id: ty
74+
name: ty check
75+
entry: uv run ty check src
76+
language: system
77+
types: [python]
78+
pass_filenames: false

.vscode/extensions.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"recommendations": [
33
"charliermarsh.ruff",
4-
"elagil.pre-commit-helper"
4+
"elagil.pre-commit-helper",
5+
"astral-sh.ty"
56
]
67
}

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ requires = ["hatchling"]
55
[dependency-groups]
66
dev = [
77
"deptry",
8+
"lxml-stubs>=0.5.1",
89
"mkdocs>=1.6.1",
910
"mkdocs-material>=9.7.1",
10-
"mkdocs-typer2>=0.1.6"
11+
"mkdocs-typer2>=0.1.6",
12+
"ty>=0.0.3"
1113
]
1214

1315
[project]

src/datasync/dms.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def guess_type(driver: str) -> str:
2828
return "image/tiff"
2929

3030

31-
def to_iso19139(metadata: dict) -> str:
31+
def to_iso19139(metadata: str) -> str | dict:
3232
loaded = orjson.loads(metadata)
3333
log.debug(loaded, id=loaded.get("identification", {}).get("identifier"))
3434
try:
@@ -123,7 +123,10 @@ def generate_csw_metadata(
123123

124124
descriptions_structure = datasets.aggregate(
125125
"json_group_structure(metadata->'$.descriptions[*]') as stucture"
126-
).fetchone()[0]
126+
).fetchone()
127+
descriptions_structure = (
128+
descriptions_structure[0] if descriptions_structure else None
129+
)
127130

128131
log.debug("Generated schema of abstract", structure=descriptions_structure)
129132

@@ -438,7 +441,10 @@ def generate_geoapi_config(
438441

439442
descriptions_structure = datasets.aggregate(
440443
"json_group_structure(metadata->'$.descriptions[*]') as stucture"
441-
).fetchone()[0]
444+
).fetchone()
445+
descriptions_structure = (
446+
descriptions_structure[0] if descriptions_structure else None
447+
)
442448

443449
descriptions = (
444450
(

src/datasync/gbif_backbone.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import fsspec
2+
import typer
3+
from duckdb import (
4+
DuckDBPyConnection,
5+
connect,
6+
)
7+
8+
from .libs.helpers import DuckDBAtomicTransaction
9+
from .settings import (
10+
env,
11+
log,
12+
)
13+
14+
log.debug("Importing GBIF Backbone settings")
15+
16+
17+
GBIF_BACKBONE_DUCKDB_NAME = env.path(
18+
"GBIF_BACKBONE_DUCKDB_FILE_NAME", default="gbif_backbone.duckdb"
19+
).root
20+
GBIF_BACKBONE_URL = env.url(
21+
"GBIF_BACKBONE_URL",
22+
default="https://hosted-datasets.gbif.org/datasets/backbone/current/backbone.zip",
23+
)
24+
25+
app = typer.Typer(help="export GBIF Backbone data to DuckDB database")
26+
27+
28+
def import_taxon(conn: DuckDBPyConnection, archive):
29+
log.debug("Importing Taxon.tsv")
30+
conn.sql("DROP TABLE IF EXISTS taxon")
31+
conn.from_csv_auto(archive.open("Taxon.tsv")).to_table("taxon")
32+
conn.sql("CREATE INDEX taxon_taxonid ON taxon (taxonid)")
33+
conn.sql("""
34+
PRAGMA create_fts_index(
35+
"taxon", "taxonID", "canonicalName", overwrite=TRUE
36+
)
37+
""")
38+
39+
40+
def import_vernacular_name(conn: DuckDBPyConnection, archive):
41+
log.debug("Importing VernacularName.tsv")
42+
conn.sql("DROP TABLE IF EXISTS vernacular_name")
43+
vernacular_names = conn.from_csv_auto(archive.open("VernacularName.tsv")) # noqa: F841
44+
conn.sql("""
45+
SELECT *, CONCAT_WS('|', taxonID, language, vernacularName) AS vernacularID
46+
FROM vernacular_names
47+
""").to_table("vernacular_name")
48+
conn.sql("CREATE INDEX vernacular_name_taxonid ON vernacular_name (taxonid)")
49+
conn.sql("CREATE INDEX vernacular_name_language ON vernacular_name (language)")
50+
conn.sql("""
51+
PRAGMA create_fts_index(
52+
"vernacular_name", "vernacularID", "vernacularName", overwrite=TRUE
53+
)
54+
""")
55+
56+
57+
@app.command()
58+
def import_all():
59+
"""Import GBIF Backbone data into a DuckDB database."""
60+
archive = fsspec.filesystem("zip", fo=GBIF_BACKBONE_URL.geturl(), mode="r")
61+
with connect(GBIF_BACKBONE_DUCKDB_NAME) as conn:
62+
with DuckDBAtomicTransaction(conn):
63+
import_taxon(conn, archive)
64+
import_vernacular_name(conn, archive)
65+
log.info("GBIF Backbone data imported successfully")

src/datasync/grass.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,25 @@ def register_layers(parquet_file_path: str, project_number: str, gisbase: str):
5959
"split_part",
6060
duckdb.ColumnExpression("cleaned_file"),
6161
duckdb.ConstantExpression("/"),
62-
duckdb.ConstantExpression(1),
62+
duckdb.ConstantExpression("1"),
6363
).alias("location"),
6464
duckdb.FunctionExpression(
6565
"split_part",
6666
duckdb.ColumnExpression("cleaned_file"),
6767
duckdb.ConstantExpression("/"),
68-
duckdb.ConstantExpression(2),
68+
duckdb.ConstantExpression("2"),
6969
).alias("mapset"),
7070
duckdb.FunctionExpression(
7171
"split_part",
7272
duckdb.ColumnExpression("cleaned_file"),
7373
duckdb.ConstantExpression("/"),
74-
duckdb.ConstantExpression(3),
74+
duckdb.ConstantExpression("3"),
7575
).alias("type"),
7676
duckdb.FunctionExpression(
7777
"split_part",
7878
duckdb.ColumnExpression("cleaned_file"),
7979
duckdb.ConstantExpression("/"),
80-
duckdb.ConstantExpression(4),
80+
duckdb.ConstantExpression("4"),
8181
).alias("resource"),
8282
]
8383
)

src/datasync/ipt/csw.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from pygeometa.schemas.iso19139 import ISO19139OutputSchema
77
from shapely.geometry import box
88

9-
from ..settings import env as logger
9+
from ..libs.helpers import get_anytext
10+
from ..settings import log
1011
from .settings import (
1112
AWS_ENDPOINT_URL,
1213
CSW_PATH,
@@ -22,29 +23,16 @@
2223
iso = ISO19139OutputSchema()
2324

2425

25-
def get_anytext(bag):
26-
"""
27-
generate bag of text for free text searches
28-
accepts list of words, string of XML, or etree.Element
29-
"""
30-
31-
if isinstance(bag, list): # list of words
32-
return " ".join([_f for _f in bag if _f]).strip()
33-
else: # xml
34-
if isinstance(bag, bytes) or isinstance(bag, str):
35-
# serialize to lxml
36-
bag = etree.fromstring(bag, PARSER) # noqa: S320
37-
# get all XML element content
38-
return " ".join([value.strip() for value in bag.xpath("//text()")])
39-
40-
4126
def eml_to_record(ds, text):
42-
metadata = eml.import_(text)
27+
metadata: dict = eml.import_(text)
4328

4429
metadata["metadata"]["identifier"] = f"ipt__{ds['id']}"
4530

4631
xml = iso.write(metadata)
47-
fts = get_anytext(xml)
32+
if isinstance(xml, str):
33+
fts = get_anytext(xml)
34+
else:
35+
raise TypeError("xml instance expected to be of type string")
4836
idf = metadata["identification"]
4937
bbox = idf["extents"]["spatial"][0]["bbox"]
5038

@@ -111,9 +99,9 @@ def eml_to_record(ds, text):
11199

112100

113101
def write_eml_record(rows):
114-
logger.info("converting to arrow")
102+
log.info("converting to arrow")
115103
records = pa.Table.from_pylist(rows) # noqa: F841
116-
logger.info("write to S3")
104+
log.info("write to S3")
117105
conn.sql("from records").write_parquet(
118106
f"s3://{S3_BUCKET}{CSW_PATH}",
119107
compression="zstd",

src/datasync/libs/helpers.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,47 @@
11
import re
22

3+
from duckdb import DuckDBPyConnection
34
from lxml import etree
5+
from lxml.etree import _Element
46

5-
PARSER = etree.XMLParser(resolve_entities=False)
7+
PARSER: etree.XMLParser = etree.XMLParser(resolve_entities=False)
68

79

8-
def get_anytext(bag: str) -> str:
10+
def get_anytext(bag: str | _Element | list[str]) -> str:
911
"""
1012
generate bag of text for free text searches
1113
accepts list of words, string of XML, or etree.Element
1214
"""
1315

1416
if isinstance(bag, list): # list of words
15-
return " ".join([_f for _f in bag if _f]).strip()
17+
return " ".join([_f for _f in bag if _f and isinstance(_f, str)]).strip()
1618
else: # xml
1719
if isinstance(bag, bytes) or isinstance(bag, str):
1820
# serialize to lxml
1921
bag = etree.fromstring(bag, PARSER) # noqa: S320
2022
# get all XML element content
21-
return re.sub(
22-
r"\s+",
23-
" ",
24-
" ".join([value.strip() for value in bag.xpath("//text()")]).strip(),
25-
)
23+
all_text = bag.xpath("//text()")
24+
if isinstance(all_text, list):
25+
return re.sub(
26+
r"\s+",
27+
" ",
28+
" ".join([str(value).strip() for value in all_text]).strip(),
29+
)
30+
# NOTE: this should never happen as the xpath evaluation always returns a list
31+
# but the type annotation is generic as xpath might return any type
32+
raise TypeError("xpath result was not a list of strings")
33+
34+
35+
class DuckDBAtomicTransaction:
36+
def __init__(self, conn: DuckDBPyConnection):
37+
self.conn = conn
38+
39+
def __enter__(self):
40+
self.conn.begin()
41+
return self.conn
42+
43+
def __exit__(self, exc_type, exc_value, traceback):
44+
if exc_type is None:
45+
self.conn.commit()
46+
else:
47+
self.conn.rollback()

src/datasync/main.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@
44

55
import typer
66

7-
from . import dms, grass, maps, ninagen, nva, pit_registering_salmon, services, ubw
7+
from . import (
8+
dms,
9+
gbif_backbone,
10+
grass,
11+
maps,
12+
ninagen,
13+
nva,
14+
pit_registering_salmon,
15+
services,
16+
ubw,
17+
)
818

919
app = typer.Typer(
1020
help="Provide subcommands for synchronizing different resources, see subcommands"
@@ -17,6 +27,7 @@
1727
app.add_typer(grass.app, name="grass-gis")
1828
app.add_typer(services.app, name="services")
1929
app.add_typer(maps.app, name="maps")
30+
app.add_typer(gbif_backbone.app, name="gbif-backbone")
2031

2132
if __name__ == "__main__":
2233
app()

src/datasync/ninagen/snp_analysis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def snp_analysis_to_parquet(file: str) -> None:
2525
"\n".join(
2626
f.read().replace("\r", "").split("\n\n")[2].split("\n")[2:]
2727
).encode()
28-
)
28+
).read()
2929
)
3030

3131
table.select("* rename (column00 as position, column01 as genlab_id)").query(

0 commit comments

Comments
 (0)