Skip to content

Commit c38bdb0

Browse files
committed
chg: make nva sync generic
1 parent ac3bac9 commit c38bdb0

10 files changed

Lines changed: 760 additions & 336 deletions

File tree

.env.example

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,12 @@ DEBUG=false
55
NVA_BASE_URL=https://api.nva.unit.no/
66
NVA_DUCKDB_FILE_NAME=nva_sync
77
NVA_INSTITUTION_CODE=7511.0.0.0
8-
NVA_ACCESS_KEY=
9-
NVA_SECRET_KEY=
10-
NVA_ENDPOINT=
11-
NVA_BUCKET=
12-
NVA_PREFIX=nva
13-
NVA_REGION=us-east-1
8+
NVA_S3_ACCESS_KEY=
9+
NVA_S3_SECRET_KEY=
10+
NVA_S3_ENDPOINT_URL=
11+
NVA_S3_BUCKET=
12+
NVA_S3_PREFIX=
1413
NVA_RESOURCES_DATA_S3_PATH=
15-
NVA_FILTER_STORAGE_ENDPOINT_URL=
16-
NVA_FILTER_STORAGE_ACCESS_KEY=
17-
NVA_FILTER_STORAGE_SECRET_KEY=
18-
NVA_FILTER_STORAGE_BUCKET=
19-
NVA_FILTER_STORAGE_PREFIX=nva-filtered
20-
NVA_FILTER_STORAGE_REGION=us-east-1
21-
NVA_FILTER_PATH_PARQUET_OUTPUT=
2214

2315
# IPT (Integrated Publishing Toolkit)
2416
IPT_URL=https://ipt.nina.no

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ uvx --from . datasync
4747
```
4848

4949
### Development
50-
Just run `uv run main.py` and you are good to go!
50+
Just run `uv run datasync --help` and see what you can do.
5151

5252
### Update from template
5353
To update your project with the latest changes from the template, run:

src/datasync/nva/__init__.py

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

5-
from . import nva, nva_filtered
5+
from . import nva, nva_filter_resources, nva_search_resources
66

77
app = typer.Typer(help="Commands to handle NVA tasks")
88

99
app.command(help="Sync NVA data from REST API to target")(nva.run)
1010

11-
app.command(help="Filter and create the tables from NVA data to parquet")(
12-
nva_filtered.filter_data
11+
app.command(help="Filter NVA parquet files and export to S3")(
12+
nva_filter_resources.filter_data
13+
)
14+
15+
app.command(help="Fetch and filter NVA data with flexible search parameters")(
16+
nva_search_resources.search_resources_api
1317
)

src/datasync/nva/nva.py

Lines changed: 44 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
import datetime
22

33
import dlt
4-
import duckdb
54
import typer
6-
from dlt.destinations.impl.filesystem.factory import filesystem
7-
from dlt.sources.credentials import AwsCredentials
85
from dlt.sources.helpers.rest_client import RESTClient
96
from dlt.sources.helpers.rest_client.paginators import JSONLinkPaginator
107

11-
from ..settings import (
12-
env,
13-
log,
8+
from ..settings import env, log
9+
from .settings import (
10+
NVA_BASE_URL,
11+
NVA_INSTITUTION_CODE,
12+
NVA_S3_ACCESS_KEY,
13+
NVA_S3_BUCKET,
14+
NVA_S3_ENDPOINT_URL,
15+
NVA_S3_PREFIX,
16+
NVA_S3_SECRET_KEY,
17+
)
18+
from .utils import (
19+
create_pipeline,
20+
create_s3_credentials,
21+
setup_duckdb_s3_connection,
22+
write_timestamp,
1423
)
1524

16-
NVA_BASE_URL = env("NVA_BASE_URL", default="https://api.nva.unit.no/")
1725
NVA_DUCKDB_NAME = env("NVA_DUCKDB_FILE_NAME", default="nva_sync")
18-
NVA_INSTITUTION_CODE = env("NVA_INSTITUTION_CODE", default="7511.0.0.0")
19-
20-
NVA_ACCESS_KEY = env("NVA_ACCESS_KEY", default="")
21-
NVA_SECRET_KEY = env("NVA_SECRET_KEY", default="")
22-
NVA_ENDPOINT = env("NVA_ENDPOINT", default="")
23-
NVA_BUCKET = env("NVA_BUCKET", default="")
24-
25-
NVA_PREFIX = env("NVA_PREFIX", default="nva")
26-
NVA_REGION = env("NVA_REGION", default="us-east-1")
2726

2827
app = typer.Typer(help="Export NVA APIs to Parquet on a S3 Bucket")
2928

@@ -95,7 +94,7 @@ def nva(
9594
get_resources(client, institution_code),
9695
name="resources",
9796
write_disposition="replace",
98-
primary_key="identifier",
97+
primary_key="id",
9998
max_table_nesting=1,
10099
)
101100

@@ -104,6 +103,7 @@ def nva(
104103
get_projects(client, institution_code),
105104
name="projects",
106105
write_disposition="replace",
106+
primary_key="id",
107107
max_table_nesting=1,
108108
)
109109

@@ -112,20 +112,23 @@ def nva(
112112
get_persons(client, institution_code),
113113
name="persons",
114114
write_disposition="replace",
115+
primary_key="id",
115116
max_table_nesting=1,
116117
)
117118
if categories:
118119
yield dlt.resource(
119120
get_categories(client),
120121
name="categories",
121122
write_disposition="replace",
123+
primary_key="_dlt_id",
122124
max_table_nesting=1,
123125
)
124126

125127
if funding_sources:
126128
yield dlt.resource(
127129
get_funding_sources(client),
128130
name="funding_sources",
131+
primary_key="identifier",
129132
write_disposition="replace",
130133
max_table_nesting=1,
131134
)
@@ -143,12 +146,12 @@ def run(
143146
base_url: str = NVA_BASE_URL,
144147
duckdb_name: str = NVA_DUCKDB_NAME,
145148
institution_code: str = NVA_INSTITUTION_CODE,
146-
endpoint_url: str = NVA_ENDPOINT,
147-
access_key: str = NVA_ACCESS_KEY,
148-
secret_key: str = NVA_SECRET_KEY,
149-
bucket: str = NVA_BUCKET,
150-
prefix: str = NVA_PREFIX,
151-
region: str = NVA_REGION,
149+
endpoint_url: str = NVA_S3_ENDPOINT_URL,
150+
access_key: str = NVA_S3_ACCESS_KEY,
151+
secret_key: str = NVA_S3_SECRET_KEY,
152+
bucket: str = NVA_S3_BUCKET,
153+
prefix: str = NVA_S3_PREFIX,
154+
region: str = "us-east-1",
152155
):
153156
if not endpoint_url:
154157
log.error("AWS S3 endpoint URL is not provided")
@@ -162,24 +165,20 @@ def run(
162165

163166
log.info("Starting NVA data sync")
164167
log.info(f"Data will be available at: {endpoint_url}/{bucket}/{prefix}")
165-
credentials = AwsCredentials(
166-
s3_url_style="path",
168+
169+
credentials = create_s3_credentials(
167170
endpoint_url=endpoint_url,
168-
aws_secret_access_key=secret_key,
169-
aws_access_key_id=access_key,
170-
region_name=region,
171+
access_key=access_key,
172+
secret_key=secret_key,
173+
region=region,
171174
)
172175

173-
pipeline = dlt.pipeline(
176+
pipeline = create_pipeline(
174177
pipeline_name=duckdb_name,
175-
destination=filesystem(
176-
region_name=region,
177-
bucket_url=f"s3://{bucket}/" + prefix,
178-
credentials=credentials,
179-
layout="{table_name}.{ext}",
180-
),
181-
dataset_name="main",
182-
progress="log",
178+
bucket=bucket,
179+
prefix=prefix,
180+
credentials=credentials,
181+
region=region,
183182
)
184183

185184
log.info(
@@ -201,28 +200,15 @@ def run(
201200
log.info("NVA data sync completed")
202201
log.info(f"Data available at: {endpoint_url}/{bucket}/{prefix}")
203202

204-
con = duckdb.connect()
205-
con.execute("INSTALL httpfs;")
206-
con.execute("LOAD httpfs;")
207-
con.execute(f"""
208-
CREATE OR REPLACE SECRET (
209-
TYPE S3,
210-
KEY_ID '{access_key}',
211-
SECRET '{secret_key}',
212-
ENDPOINT '{endpoint_url.replace("https://", "").replace("http://", "")}',
213-
REGION '{region}',
214-
URL_STYLE 'path'
215-
);
216-
""")
217-
218-
timestamp = datetime.datetime.now().isoformat()
219-
con.execute(f"""
220-
COPY (SELECT '{timestamp}' as last_successful_run)
221-
TO 's3://{bucket}/{prefix}/last_successful_run.parquet'
222-
(FORMAT PARQUET, COMPRESSION ZSTD)
223-
""")
203+
con = setup_duckdb_s3_connection(
204+
endpoint_url=endpoint_url,
205+
access_key=access_key,
206+
secret_key=secret_key,
207+
region=region,
208+
)
209+
210+
write_timestamp(con, bucket, prefix)
224211
con.close()
225-
log.info(f"Last successful run timestamp written: {timestamp}")
226212

227213

228214
if __name__ == "__main__":

0 commit comments

Comments
 (0)