Skip to content

Commit 69dbede

Browse files
committed
chg: add sync script for publications per researcher
1 parent 12ba31d commit 69dbede

7 files changed

Lines changed: 324 additions & 27 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ dependencies = [
4444
"pyyaml>=6.0.3",
4545
"deepdiff>=8.6.1",
4646
"xmldiff>=2.7.0",
47-
"xmlschema>=4.3.1"
47+
"xmlschema>=4.3.1",
48+
"pandas>=2.3.3"
4849
]
4950
description = ""
5051
license = "GPL-3.0+"
@@ -63,7 +64,7 @@ ubw_sync = "datasync.ubw:app"
6364
known_first_party = ["src"]
6465

6566
[tool.deptry.per_rule_ignores]
66-
DEP002 = ["rich", "numpy", "setuptools"]
67+
DEP002 = ["rich", "numpy", "setuptools", "pandas"]
6768

6869
[tool.hatch.metadata]
6970
allow-direct-references = true

src/datasync/nva/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
# this allows to attach the scripts to the app
33
import typer
44

5-
from . import nva, nva_filter_resources, nva_search_resources
5+
from . import (
6+
nva,
7+
nva_filter_resources,
8+
nva_get_publications_per_researcher,
9+
nva_search_resources,
10+
)
611

712
app = typer.Typer(help="Commands to handle NVA tasks")
813

@@ -15,3 +20,7 @@
1520
app.command(help="Fetch and filter NVA data with flexible search parameters")(
1621
nva_search_resources.search_resources_api
1722
)
23+
24+
app.command(help="Fetch publications for researchers at NINA")(
25+
nva_get_publications_per_researcher.get_pubs_per_researcher
26+
)

src/datasync/nva/nva.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ def run(
189189
bucket=bucket,
190190
prefix=prefix,
191191
credentials=credentials,
192-
region=region,
193192
)
194193

195194
log.info(
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# get nva id's https://s3-ext-1.nina.no/dms/nina/employees.parquet
2+
# https://api.nva.unit.no/search/resources?contributor=https://api.nva.unit.no/cristin/person/408357
3+
# use search_api function in nva_search_resources.py
4+
import typer
5+
6+
from ..settings import (
7+
log,
8+
)
9+
from .nva_search_resources import search_resources_api
10+
from .utils import (
11+
setup_duckdb_s3_connection,
12+
write_timestamp,
13+
)
14+
15+
app = typer.Typer(help="Fetching publications from researchers at NINA")
16+
17+
18+
def _fetch_publications_for_researcher(
19+
nva_id: str,
20+
storage_s3_path: str,
21+
storage_access_key: str,
22+
storage_secret_key: str,
23+
storage_bucket: str,
24+
storage_prefix: str,
25+
storage_region: str,
26+
) -> str:
27+
log.debug("Fetching publications for researcher", nva_id=nva_id)
28+
search_resources_api(
29+
resource_name=f"publications_{nva_id}",
30+
filters=[f"contributor=https://api.nva.unit.no/cristin/person/{nva_id}"],
31+
apply_filter=False,
32+
storage_endpoint_url=storage_s3_path,
33+
storage_access_key=storage_access_key,
34+
storage_secret_key=storage_secret_key,
35+
storage_bucket=storage_bucket,
36+
storage_prefix=f"{storage_prefix}/{nva_id}/",
37+
storage_region=storage_region,
38+
add_timestamp=False,
39+
)
40+
return nva_id
41+
42+
43+
@app.command()
44+
def get_pubs_per_researcher(
45+
employees_parquet: str = typer.Option(
46+
...,
47+
envvar="NINA_EMPLOYEES_PARQUET",
48+
help="Path to employees parquet file",
49+
),
50+
storage_s3_path: str = typer.Option(
51+
...,
52+
envvar="NVA_S3_ENDPOINT_URL",
53+
help="S3 endpoint URL for storage",
54+
),
55+
storage_access_key: str = typer.Option(
56+
..., envvar="NVA_S3_ACCESS_KEY", help="S3 access key for storage"
57+
),
58+
storage_secret_key: str = typer.Option(
59+
..., envvar="NVA_S3_SECRET_KEY", help="S3 secret key for storage"
60+
),
61+
storage_bucket: str = typer.Option(
62+
..., envvar="NVA_S3_BUCKET", help="S3 bucket for storage"
63+
),
64+
storage_prefix: str = typer.Option(
65+
..., envvar="NVA_S3_PREFIX", help="S3 prefix for storage"
66+
),
67+
storage_region: str = typer.Option(
68+
"us-east-1", envvar="NVA_S3_REGION", help="S3 region for storage"
69+
),
70+
):
71+
log.debug("Fetching ALL publications from researchers at NINA")
72+
73+
con = setup_duckdb_s3_connection(
74+
endpoint_url=storage_s3_path,
75+
access_key=storage_access_key,
76+
secret_key=storage_secret_key,
77+
region=storage_region,
78+
)
79+
80+
employees = con.read_parquet(employees_parquet).to_df()
81+
82+
# get all employees with missing nva_id and log their names for debugging
83+
missing_nva_id = employees["nva_id"].isna().sum()
84+
if missing_nva_id > 0:
85+
missing_names = (
86+
employees[employees["nva_id"].isna()]["firstname"]
87+
+ " "
88+
+ employees[employees["nva_id"].isna()]["lastname"]
89+
).tolist()
90+
log.warning(
91+
"Employees missing NVA IDs",
92+
missing_count=missing_nva_id,
93+
total_employees=len(employees),
94+
missing_names=missing_names,
95+
)
96+
con.close()
97+
nva_ids = [str(nva_id) for nva_id in employees["nva_id"].dropna().unique()]
98+
99+
failed = []
100+
for nva_id in nva_ids:
101+
try:
102+
_fetch_publications_for_researcher(
103+
nva_id,
104+
storage_s3_path,
105+
storage_access_key,
106+
storage_secret_key,
107+
storage_bucket,
108+
storage_prefix,
109+
storage_region,
110+
)
111+
except Exception as e:
112+
log.error(
113+
"Failed to fetch publications for researcher",
114+
nva_id=nva_id,
115+
error=str(e),
116+
)
117+
failed.append(nva_id)
118+
119+
if failed:
120+
log.warning(
121+
"Some researchers failed to fetch", failed=failed, count=len(failed)
122+
)
123+
124+
log.info(
125+
"Fetched all publications for researchers",
126+
total_researchers=len(nva_ids),
127+
failed=len(failed),
128+
succeeded=len(nva_ids) - len(failed),
129+
)
130+
131+
con = setup_duckdb_s3_connection(
132+
endpoint_url=storage_s3_path,
133+
access_key=storage_access_key,
134+
secret_key=storage_secret_key,
135+
region=storage_region,
136+
)
137+
write_timestamp(con, storage_bucket, storage_prefix)
138+
con.close()

src/datasync/nva/nva_search_resources.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def search_resources_api(
5959
storage_region: str = typer.Option(
6060
"us-east-1", "--storage-region", help="S3 region for storage"
6161
),
62+
add_timestamp: bool = typer.Option(
63+
True, help="Whether to write last successful run timestamp to S3"
64+
),
6265
):
6366
"""
6467
Get resources from NVA API
@@ -128,7 +131,7 @@ def search_resources_api(
128131
log.error("Valid filters", valid_filters=VALID_PARAMS_NVA_API)
129132
raise typer.Exit(code=1)
130133

131-
log.info(f"Fetching NVA data with search parameters: {search_params}")
134+
log.debug(f"Fetching NVA data with search parameters: {search_params}")
132135

133136
credentials = create_s3_credentials(
134137
endpoint_url=storage_endpoint_url,
@@ -142,11 +145,10 @@ def search_resources_api(
142145
bucket=storage_bucket,
143146
prefix=storage_prefix,
144147
credentials=credentials,
145-
region=storage_region,
146148
)
147149

148-
log.info(f"Fetching resources from {base_url}")
149-
pipeline.run(
150+
log.debug(f"Fetching resources from {base_url}")
151+
load_info = pipeline.run(
150152
nva_search_source(
151153
base_url=base_url,
152154
resource_name=resource_name,
@@ -156,14 +158,15 @@ def search_resources_api(
156158
loader_file_format="parquet",
157159
)
158160

159-
if apply_filter:
160-
con = setup_duckdb_s3_connection(
161-
endpoint_url=storage_endpoint_url,
162-
access_key=storage_access_key,
163-
secret_key=storage_secret_key,
164-
region=storage_region,
165-
)
161+
log.debug("Pipeline run completed", load_info=load_info)
166162

163+
con = setup_duckdb_s3_connection(
164+
endpoint_url=storage_endpoint_url,
165+
access_key=storage_access_key,
166+
secret_key=storage_secret_key,
167+
region=storage_region,
168+
)
169+
if apply_filter:
167170
apply_filter_transformation(
168171
con=con,
169172
bucket=storage_bucket,
@@ -172,11 +175,14 @@ def search_resources_api(
172175
check_additional_identifiers=True,
173176
)
174177

175-
write_timestamp(con, storage_bucket, storage_prefix)
178+
if add_timestamp:
179+
write_timestamp(con, storage_bucket, storage_prefix)
176180
con.close()
177181

182+
log.debug("NVA data sync completed")
178183
log.info(
179-
f"Data available at: {storage_endpoint_url}/{storage_bucket}/{storage_prefix}/"
184+
f"Data available at: {storage_endpoint_url}/{storage_bucket}/"
185+
f"{storage_prefix.rstrip('/')}/"
180186
f"main/{resource_name}.parquet"
181187
)
182188

src/datasync/nva/utils.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def get_nva_resources(
2626
Yields:
2727
Resources from the NVA API
2828
"""
29-
log.info(f"Fetching resources with params: {search_params}")
29+
log.debug(f"Fetching resources with params: {search_params}")
3030
yield from client.paginate(
3131
"search/resources",
3232
method="GET",
@@ -99,19 +99,16 @@ def create_pipeline(
9999
bucket: str,
100100
prefix: str,
101101
credentials: AwsCredentials,
102-
region: str,
103102
) -> dlt.Pipeline:
104103
"""Create a DLT pipeline with filesystem destination."""
105104
return dlt.pipeline(
106105
pipeline_name=pipeline_name,
107106
destination=filesystem(
108-
region_name=region,
109-
bucket_url=f"s3://{bucket}/{prefix}",
107+
bucket_url=f"s3://{bucket}/{prefix.rstrip('/')}",
110108
credentials=credentials,
111109
layout="{table_name}.{ext}",
112110
),
113111
dataset_name="main",
114-
progress="log",
115112
)
116113

117114

@@ -150,6 +147,7 @@ def write_timestamp(
150147
) -> str:
151148
"""Write last successful run timestamp to S3."""
152149
timestamp = datetime.datetime.now().isoformat()
150+
prefix = prefix.rstrip("/")
153151
con.execute(f"""
154152
COPY (SELECT '{timestamp}' as last_successful_run)
155153
TO 's3://{bucket}/{prefix}/last_successful_run.parquet'

0 commit comments

Comments
 (0)