Skip to content

Commit d9497b4

Browse files
committed
feat: dms generate pycsw metadata
1 parent af864ea commit d9497b4

4 files changed

Lines changed: 2007 additions & 1354 deletions

File tree

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ dependencies = [
1515
"numpy>=2.2.6",
1616
"structlog>=25.5.0",
1717
"rich>=14.2.0",
18-
"typer>=0.20.0"
18+
"typer>=0.20.0",
19+
"pygeometa>=0.19.0",
20+
"orjson>=3.11.4",
21+
"duckdb>=1.4.2"
1922
]
2023
description = ""
2124
license = "GPL-3.0+"

src/datasync/dms.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import duckdb
2+
import orjson
3+
import typer
4+
from pygeometa.schemas.iso19139 import ISO19139OutputSchema
5+
6+
from .settings import log
7+
8+
app = typer.Typer()
9+
10+
iso = ISO19139OutputSchema()
11+
12+
13+
def to_iso19139(metadata: dict) -> str:
14+
loaded = orjson.loads(metadata)
15+
log.debug(loaded, id=loaded.get("identification").get("identifier"))
16+
try:
17+
xml = iso.write(loaded)
18+
return xml
19+
except Exception as e:
20+
log.exception(e, id=loaded.get("identification").get("identifier"))
21+
return ""
22+
23+
24+
@app.command()
25+
def generate_csw_metadata(
26+
base_url: str = "https://s3-int-1/dms-test/dms/tables/", lang="en"
27+
):
28+
# TODO: localize the output based on the language in input
29+
30+
conn = duckdb.connect()
31+
conn.sql("INSTALL yaml FROM community; LOAD yaml; Install spatial; load spatial")
32+
33+
datasets = conn.read_parquet(base_url + "datasets_dataset.parquet").filter(
34+
"json_keys(metadata) <> []"
35+
)
36+
log.debug(datasets)
37+
rasters = conn.read_parquet(base_url + "datasets_rasterresource.parquet") # noqa: F841
38+
vectors = conn.read_parquet(base_url + "datasets_tabularresource.parquet") # noqa: F841
39+
contributions = conn.read_parquet(
40+
base_url + "datasets_datasetcontribution.parquet"
41+
).select("* replace (split(rtrim(ltrim(roles, '{'), '}'), ',') as roles)")
42+
43+
resources = conn.sql("""
44+
from rasters
45+
select * replace (ST_GEomFromHEXWKB(extent) as extent),
46+
'raster' as type
47+
union all by name
48+
from vectors select * replace (ST_GEomFromHEXWKB(extent) as extent),
49+
case when extent is null then 'table' else 'vector' end as type
50+
""")
51+
log.debug(resources)
52+
53+
metadata = resources.select(f"""
54+
id,
55+
dataset_id,
56+
{{
57+
-- TODO: investigate language
58+
language: '{lang}',
59+
charset: 'utf8',
60+
hierarchylevel: case
61+
when extent is not null then 'dataset' else 'nonGeographicDataset'
62+
end,
63+
datestamp: current_localtimestamp()::date,
64+
-- TODO: this needs some fixes
65+
dataseturi: uri,
66+
}}::json as metadata
67+
""")
68+
69+
log.debug(metadata)
70+
71+
spatial = resources.select("""
72+
id,
73+
{
74+
datatype: case
75+
when type = 'table' then 'textTable'
76+
when type = 'raster' then 'grid'
77+
else 'vector'
78+
end,
79+
-- this needs to be fixed in a smarter way
80+
geomtype: 'point',
81+
}::json as spatial
82+
""")
83+
84+
log.debug(spatial)
85+
86+
identification = resources.select("""
87+
id,
88+
{
89+
identifier: id,
90+
-- TODO: these needs to be read from the dataset
91+
language: metadata->'language',
92+
-- TODO: these is probably a combination
93+
title: title,
94+
fee: 'None',
95+
status: 'completed',
96+
-- TODO: these needs to be read from the dataset
97+
rights: '',
98+
-- TODO: these needs to be read from the dataset
99+
abstract: '',
100+
url: uri,
101+
dates: {
102+
creation: created_at,
103+
revision: last_modified_at
104+
},
105+
extents: {
106+
spatial: case when extent is not null then [
107+
{
108+
bbox: [
109+
ST_XMIN(extent), ST_YMIN(extent),
110+
ST_XMAX(extent), ST_YMAX(extent),
111+
],
112+
crs: 4326
113+
}
114+
] else [{
115+
-- default to Norway BBOX
116+
bbox: [
117+
4.99207807783, 58.0788841824, 31.29341841, 80.6571442736
118+
],
119+
crs: 4326
120+
}]
121+
end
122+
},
123+
license: {
124+
-- need to join with dataset to read those!
125+
"name": coalesce(metadata->>'$.rightsList[0].rights', 'All rights reserved'),
126+
"uri": coalesce(metadata->>'$.rightsList[0].rightsUri', ''),
127+
},
128+
keywords: {
129+
"default": {
130+
keywords_type: 'theme',
131+
keywords: [],
132+
}
133+
},
134+
}::json as identification
135+
""") # noqa: E501
136+
137+
log.debug(identification)
138+
139+
content_info = resources.select("""
140+
id,
141+
{
142+
type: case
143+
when type = 'raster' then 'coverage'
144+
else 'feature_catalogue'
145+
end,
146+
dimensions: [],
147+
}::json as content_info
148+
""")
149+
150+
log.debug(content_info)
151+
152+
contacts = (
153+
contributions.select("""
154+
dataset_id,
155+
unnest(roles) as role,
156+
{
157+
organization: 'Norsk institutt for naturforsking',
158+
individualname: last_name || ', ' || first_name,
159+
-- default to NINA
160+
phone: '' ,
161+
positionname: '',
162+
fax: '',
163+
address: '',
164+
postalcode: '',
165+
country: 'Norway',
166+
email: email,
167+
url: 'https://www.nina.no',
168+
city: 'Trondheim'
169+
} as contact,
170+
""")
171+
.select(
172+
"dataset_id, role, contact, row_number() over (partition by dataset_id, role order by contact->>'individualname') as role_order" # noqa: E501
173+
)
174+
.aggregate(
175+
"dataset_id, json_group_object(lower(role[1]) || role[2:] || case when role_order = 1 then '' else ('_' || role_order) end, contact) as contact", # noqa: E501
176+
group_expr="dataset_id",
177+
)
178+
)
179+
180+
log.debug(contacts)
181+
182+
distribution = resources.select("""
183+
id,
184+
{
185+
file: {
186+
url: uri,
187+
type: case when type = 'raster' then 'FILE:RASTER'
188+
when type = 'vector' then 'FILE:GEO'
189+
else 'download'
190+
end,
191+
name: title,
192+
description: description,
193+
function: 'download'
194+
}
195+
}::json as distribution
196+
""")
197+
198+
log.debug(distribution)
199+
200+
res = conn.sql(
201+
"""
202+
select m.id, {
203+
mcf: { version: 1.0 },
204+
metadata: m.metadata,
205+
identification: i.identification,
206+
content_info: ci.content_info,
207+
spatial: sp.spatial,
208+
distribution: d.distribution,
209+
contact: c.contact
210+
}::json as metadata
211+
from metadata as m
212+
join identification as i on m.id = i.id
213+
join content_info as ci on ci.id = m.id
214+
join spatial as sp on sp.id = m.id
215+
join distribution as d on d.id = m.id
216+
join contacts as c on m.dataset_id = c.dataset_id
217+
218+
"""
219+
)
220+
log.debug(res)
221+
222+
conn.create_function("to_iso19139", to_iso19139)
223+
224+
log.debug(res.select("id, to_iso19139(metadata) as xml"))
225+
226+
227+
if __name__ == "__main__":
228+
app()

src/datasync/main.py

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

55
import typer
66

7-
from . import nva, ubw
7+
from . import dms, nva, ubw
88

99
app = typer.Typer()
1010
app.add_typer(nva.app, name="nva")
1111
app.add_typer(ubw.app, name="ubw")
12+
app.add_typer(dms.app, name="dms")
1213

1314
if __name__ == "__main__":
1415
app()

0 commit comments

Comments
 (0)