Skip to content

Commit 736275b

Browse files
authored
Merge pull request #144 from geoadmin/develop
New Release v0.20.0 - #minor
2 parents 863ac03 + 5d8f6cb commit 736275b

17 files changed

Lines changed: 3677 additions & 750 deletions

File tree

.pylintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ missing-member-max-choices=1
181181
# List of decorators that change the signature of a decorated function.
182182
signature-mutators=
183183

184+
# enable certain c-extensions
185+
extension-pkg-allow-list=lxml
186+
184187

185188
[FORMAT]
186189

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,16 @@ ci:
6060
# Create virtual env with all packages for development using the Pipfile.lock
6161
pipenv sync --dev
6262

63+
env:
64+
ifeq ("$(wildcard ./.env)","")
65+
@echo ".env file not found, copying it from .env.default..."
66+
else
67+
@echo ".env file found, leaving untouched..."
68+
endif
69+
6370
.PHONY: setup
64-
setup: $(SETTINGS_TIMESTAMP) ## Create virtualenv with all packages for development
71+
setup: $(SETTINGS_TIMESTAMP) env ## Create virtualenv with all packages for development
6572
pipenv install --dev
66-
cp .env.default .env
6773
pipenv shell
6874

6975
.PHONY: format

Pipfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ opentelemetry-instrumentation-django = "0.60b1"
2727
opentelemetry-instrumentation-logging = "0.60b1"
2828
opentelemetry-instrumentation-psycopg = "0.60b1"
2929
opentelemetry-instrumentation-system-metrics = "0.60b1"
30+
dyntastic = "~=0.18.0"
31+
lxml = "~=6.0"
32+
rdflib = "~=7.6"
3033

3134
[dev-packages]
3235
yapf = "*"
@@ -45,13 +48,14 @@ mypy = "*"
4548
debugpy = "*"
4649
bandit = "*"
4750
django-stubs = "~=5.2"
48-
boto3-stubs = {extras = ["cognito-idp"], version = "~=1.37"}
51+
boto3-stubs = {extras = ["cognito-idp","dynamodb"], version = "~=1.37"}
4952
pytest-xdist = "*"
5053
pytest-cov = "*"
5154
types-nanoid = "~=2.0"
5255
types-gevent = "~=25.4"
5356
types-requests = "~=2.32"
5457
types-gunicorn = "~=23.0"
58+
lxml-stubs = "~=0.5"
5559

5660
[requires]
5761
python_version = "3.12"

Pipfile.lock

Lines changed: 862 additions & 639 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/bod/management/commands/bod_sync.py

Lines changed: 129 additions & 108 deletions
Large diffs are not rendered by default.

app/distributions/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class DatasetAdmin(admin.ModelAdmin): # type:ignore[type-arg]
2424

2525
list_display = ('dataset_id', 'title_en', 'provider')
2626
list_filter = (('provider', admin.RelatedOnlyFieldListFilter),)
27+
search_fields = ('dataset_id',)
2728
readonly_fields = ('created', 'updated')
2829

2930
def get_form(

app/distributions/export_models.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from typing import Any
2+
3+
from pydantic import BaseModel
4+
5+
6+
class BaseModelWithDynamoDBSerialization(BaseModel):
7+
"""BaseModel with custom serialization for DynamoDB"""
8+
9+
def as_dynamodb_item(self) -> dict[str, Any]:
10+
"""Convert the dataset to a DynamoDB item format
11+
12+
Returns:
13+
dict[str, Any]: The dataset represented as a DynamoDB item.
14+
"""
15+
16+
def serialize(value: Any) -> dict[str, Any]:
17+
if value is None:
18+
return {"NULL": True}
19+
if isinstance(value, int):
20+
return {"N": str(value)}
21+
if isinstance(value, str):
22+
return {"S": value}
23+
if isinstance(value, list):
24+
return {"L": [serialize(i) for i in value]}
25+
if isinstance(value, dict):
26+
return {"M": {k: serialize(v) for k, v in value.items()}}
27+
raise ValueError(f"Unexpected type {type(value)}")
28+
29+
item = self.model_dump(mode="json")
30+
return {key: serialize(value) for key, value in item.items()}
31+
32+
33+
class ExportDataset(BaseModelWithDynamoDBSerialization):
34+
dataset_id: str
35+
title_de: str
36+
title_fr: str
37+
title_en: str
38+
title_it: str | None
39+
title_rm: str | None
40+
description_de: str
41+
description_fr: str
42+
description_en: str
43+
description_it: str | None
44+
description_rm: str | None
45+
attribution: list[str]
46+
provider: list[str]
47+
created: str
48+
updated: str
49+
geocat_id: str
50+
_legacy_id: int
51+
52+
53+
class ExportProvider(BaseModelWithDynamoDBSerialization):
54+
provider_id: str
55+
created: str
56+
updated: str
57+
name_de: str
58+
name_fr: str
59+
name_en: str
60+
name_it: str | None
61+
name_rm: str | None
62+
acronym_de: str
63+
acronym_fr: str
64+
acronym_en: str
65+
acronym_it: str | None
66+
acronym_rm: str | None
67+
_legacy_id: int
68+
69+
70+
class Keyword(BaseModel):
71+
type: str | None
72+
thesaurus: str | None
73+
concept: str | None
74+
translation_de: str | None
75+
translation_fr: str | None
76+
translation_en: str | None
77+
translation_it: str | None
78+
translation_rm: str | None
79+
80+
81+
class KeywordList(BaseModelWithDynamoDBSerialization):
82+
dataset_id: str
83+
geocat_id: str
84+
keywords: list[Keyword]
85+
86+
87+
class OnlineResource(BaseModel):
88+
url: str | None
89+
url_de: str | None
90+
url_fr: str | None
91+
url_en: str | None
92+
url_it: str | None
93+
url_rm: str | None
94+
protocol: str | None
95+
name_de: str | None
96+
name_fr: str | None
97+
name_en: str | None
98+
name_it: str | None
99+
name_rm: str | None
100+
description_de: str | None
101+
description_fr: str | None
102+
description_en: str | None
103+
description_it: str | None
104+
description_rm: str | None
105+
106+
107+
class Contact(BaseModel):
108+
role: str | None
109+
org_name: str | None
110+
org_name_de: str | None
111+
org_name_fr: str | None
112+
org_name_en: str | None
113+
org_name_it: str | None
114+
org_name_rm: str | None
115+
org_acronym: str | None
116+
org_acronym_de: str | None
117+
org_acronym_fr: str | None
118+
org_acronym_en: str | None
119+
org_acronym_it: str | None
120+
org_acronym_rm: str | None
121+
org_email: str | None
122+
position_name_de: str | None
123+
position_name_fr: str | None
124+
position_name_en: str | None
125+
position_name_it: str | None
126+
position_name_rm: str | None
127+
individual_name: str | None
128+
individual_first_name: str | None
129+
individual_last_name: str | None
130+
contact_direct_number: str | None
131+
contact_voice: str | None
132+
contact_facsimile: str | None
133+
contact_city: str | None
134+
contact_administrative_area: str | None
135+
contact_postal_code: str | None
136+
contact_country: str | None
137+
contact_electronic_mail_address: str | None
138+
contact_street_name: str | None
139+
contact_street_number: str | None
140+
contact_post_box: str | None
141+
hours_of_service: str | None
142+
contact_instructions: str | None
143+
online_resources: list[OnlineResource]
144+
145+
146+
class ContactList(BaseModelWithDynamoDBSerialization):
147+
dataset_id: str
148+
geocat_id: str
149+
contacts: list[Contact]

app/distributions/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import json
2+
from typing import Any
3+
4+
import boto3
5+
import environ
6+
from distributions.export_models import ExportDataset
7+
from distributions.export_models import ExportProvider
8+
from distributions.models import Dataset
9+
from provider.models import Provider
10+
from utils.command import CustomBaseCommand
11+
12+
from django.core import serializers
13+
from django.core.management.base import CommandParser
14+
15+
env = environ.Env()
16+
boto3.setup_default_session(profile_name="swisstopo-swissgeo-dev")
17+
18+
SAMPLE_IDS = [
19+
"ch.bafu.schutzgebiete-luftfahrt",
20+
"ch.swisstopo.lubis-luftbilder-dritte-kantone",
21+
"ch.bav.sachplan-infrastruktur-schiene_anhorung",
22+
"ch.agroscope.korridore-feuchtgebietsarten_qualitaet",
23+
"ch.meteoschweiz.messwerte-pollen-buche-1h",
24+
]
25+
26+
27+
class Command(CustomBaseCommand):
28+
help = "Exports datasets from the system to DynamoDB"
29+
30+
def add_arguments(self, parser: CommandParser) -> None:
31+
super().add_arguments(parser)
32+
parser.add_argument(
33+
"--clear",
34+
action="store_true",
35+
help="Delete existing objects before importing",
36+
)
37+
parser.add_argument(
38+
"--dry-run",
39+
action="store_true",
40+
help="Dry run, abort transaction in the end",
41+
)
42+
parser.add_argument(
43+
"--sample",
44+
action="store_true",
45+
help="Export a sample of datasets (10) instead of all datasets",
46+
)
47+
parser.add_argument(
48+
"--providers",
49+
action="store_true",
50+
help="Import providers",
51+
)
52+
parser.add_argument(
53+
"--attributions",
54+
action="store_true",
55+
help="Import attributions",
56+
)
57+
parser.add_argument(
58+
"--datasets",
59+
action="store_true",
60+
help="Import datasets",
61+
)
62+
parser.add_argument(
63+
"--target-env",
64+
type=str,
65+
choices=["dev", "int", "prod"],
66+
default="dev",
67+
help="Specify the target environment",
68+
)
69+
parser.add_argument(
70+
"--profile-name",
71+
type=str,
72+
nargs="?",
73+
default=None,
74+
help="Specify the profile name (only needed locally)",
75+
)
76+
parser.add_argument(
77+
"--debug",
78+
action="store_true",
79+
help="Enable debug logging",
80+
)
81+
82+
def handle(self, *args: Any, **options: Any) -> None:
83+
"""Main entry point of command."""
84+
if options["profile_name"]:
85+
self.session = boto3.Session(profile_name=options["profile_name"]) # pylint: disable=attribute-defined-outside-init
86+
else:
87+
self.session = boto3.Session() # pylint: disable=attribute-defined-outside-init
88+
89+
if options["datasets"]:
90+
self.export_datasets(*args, **options)
91+
if options["providers"]:
92+
self.export_providers(*args, **options)
93+
94+
def export_datasets(self, *args: Any, **options: Any) -> None:
95+
96+
dynamodb_client = self.session.client("dynamodb", region_name="eu-central-1")
97+
98+
if options["sample"]:
99+
self.print("Exporting only a sample of datasets (10)...")
100+
qs = Dataset.objects.filter(dataset_id__in=SAMPLE_IDS)
101+
else:
102+
qs = Dataset.objects.all()
103+
datasets = json.loads(
104+
serializers.serialize(
105+
"json", qs, use_natural_foreign_keys=True, use_natural_primary_keys=True
106+
)
107+
)
108+
109+
for dataset in datasets:
110+
exp_item = ExportDataset(**dataset["fields"])
111+
item = exp_item.as_dynamodb_item()
112+
self.print(f"Exporting dataset {exp_item.dataset_id} to DynamoDB")
113+
dynamodb_client.put_item(
114+
TableName=f"harvest-datasets-{options['target_env']}", Item=item
115+
)
116+
117+
def export_providers(self, *args: Any, **options: Any) -> None:
118+
dynamodb_client = self.session.client("dynamodb", region_name="eu-central-1")
119+
120+
qs = Provider.objects.all()
121+
providers = json.loads(
122+
serializers.serialize(
123+
"json", qs, use_natural_foreign_keys=True, use_natural_primary_keys=True
124+
)
125+
)
126+
127+
for provider in providers:
128+
exp_item = ExportProvider(**provider["fields"])
129+
item = exp_item.as_dynamodb_item()
130+
self.print(f"Exporting provider {exp_item.provider_id} to DynamoDB")
131+
dynamodb_client.put_item(
132+
TableName=f"harvest-providers-{options['target_env']}", Item=item
133+
)

0 commit comments

Comments
 (0)