|
34 | 34 | "dataset_description": "notes", |
35 | 35 | "dataset_publisher_name": "dataset_publisher_name", |
36 | 36 | "dataset_publisher_mbox": "dataset_publisher_mbox", |
| 37 | + "dataset_contactPoint_fn": "dataset_contactPoint_fn", |
| 38 | + "dataset_contactPoint_hasEmail": "dataset_contactPoint_hasEmail", |
37 | 39 | "dataset_superTheme": "dataset_superTheme", |
38 | 40 | "dataset_theme": "dataset_theme", |
39 | 41 | "dataset_keyword": "dataset_keywords", |
|
43 | 45 | "dataset_language": "dataset_language", |
44 | 46 | "dataset_spatial": "spatial", |
45 | 47 | "dataset_temporal": "temporal_start", |
| 48 | + "dataset_landingPage": "dataset_landingPage", |
46 | 49 | "dataset_license": "license_id", |
47 | 50 | "dataset_source": "dataset_source", |
48 | 51 | } |
|
53 | 56 | "distribution_title": "name", |
54 | 57 | "distribution_description": "description", |
55 | 58 | "distribution_downloadURL": "url", |
| 59 | + "distribution_fileName": "distribution_fileName", |
56 | 60 | "distribution_format": "format", |
57 | 61 | "distribution_mediaType": "mimetype", |
58 | | - "distribution_modified":"last_modified", |
59 | | - "distribution_issued" : "created" |
| 62 | + "distribution_license": "distribution_license", |
| 63 | + "distribution_byteSize": "distribution_byteSize", |
| 64 | + "distribution_modified": "last_modified", |
| 65 | + "distribution_issued": "created", |
| 66 | + "distribution_rights": "distribution_rights", |
60 | 67 | } |
61 | 68 |
|
62 | 69 | # ----------------------------------------------------------------------------- |
|
99 | 106 | "distribution_wfs_url": "wfs_url", # URL servicio WFS |
100 | 107 | } |
101 | 108 |
|
102 | | -DEFAULT_DATASET_SHEET = "dataset" |
103 | | -DEFAULT_DISTRIBUTION_SHEET = "distribution" |
| 109 | +DEFAULT_DATASET_SHEET = ["dataset", "Dataset", "datasets", "Datasets"] |
| 110 | +DEFAULT_DISTRIBUTION_SHEET = ["distribution", "Distribution", "distributions", "Distributions"] |
104 | 111 |
|
105 | | -DATASET_REQUIRED = {} |
| 112 | + |
| 113 | +def _resolve_sheet_name(wb, candidates): |
| 114 | + """Retorna el primer nombre de hoja del workbook que coincida con alguno de los candidatos.""" |
| 115 | + if isinstance(candidates, str): |
| 116 | + candidates = [candidates] |
| 117 | + return next((s for s in wb.sheetnames if s in candidates), None) |
| 118 | + |
| 119 | +DATASET_REQUIRED = {"title"} |
106 | 120 | DISTRIBUTION_REQUIRED = {"url"} |
107 | 121 |
|
108 | | -# Columnas del mapping V1 que pueden no estar en el Excel (opcionales) |
| 122 | +# Columnas del mapping V1 que pueden no estar en el Excel (opcionales/recomendadas) |
109 | 123 | # La validación de headers será permisiva para estas. |
110 | | -ANDINO_V1_DATASET_OPTIONAL = set() |
111 | | -ANDINO_V1_RESOURCE_OPTIONAL = set() |
| 124 | +ANDINO_V1_DATASET_OPTIONAL = { |
| 125 | + "dataset_publisher_mbox", |
| 126 | + "dataset_contactPoint_fn", |
| 127 | + "dataset_contactPoint_hasEmail", |
| 128 | + "dataset_theme", |
| 129 | + "dataset_keyword", |
| 130 | + "dataset_modified", |
| 131 | + "dataset_language", |
| 132 | + "dataset_spatial", |
| 133 | + "dataset_temporal", |
| 134 | + "dataset_landingPage", |
| 135 | + "dataset_license", |
| 136 | +} |
| 137 | +ANDINO_V1_RESOURCE_OPTIONAL = { |
| 138 | + "distribution_description", |
| 139 | + "distribution_fileName", |
| 140 | + "distribution_format", |
| 141 | + "distribution_mediaType", |
| 142 | + "distribution_license", |
| 143 | + "distribution_byteSize", |
| 144 | + "distribution_modified", |
| 145 | + "distribution_rights", |
| 146 | +} |
112 | 147 |
|
113 | 148 |
|
114 | 149 | class XLSXHarvester(HarvesterBase): |
@@ -246,6 +281,8 @@ def _validate_mapping_headers(self, workbook, sheet_name, mapping, skip=0, optio |
246 | 281 | """ |
247 | 282 | optional_keys = {k.lower() for k in (optional_keys or set())} |
248 | 283 |
|
| 284 | + if sheet_name is None: |
| 285 | + raise ValueError("No existe la hoja '%s'" % sheet_name) |
249 | 286 | try: |
250 | 287 | ws = workbook[sheet_name] |
251 | 288 | except KeyError: |
@@ -308,9 +345,9 @@ def gather_stage(self, harvest_job): |
308 | 345 | ) |
309 | 346 | return [] |
310 | 347 |
|
311 | | - skip = self.config.get("skip_rows", 0) |
312 | | - ds_sheet = self.config.get("dataset_sheet", DEFAULT_DATASET_SHEET) |
313 | | - dist_sheet = self.config.get("distribution_sheet", DEFAULT_DISTRIBUTION_SHEET) |
| 348 | + skip = self.config.get("skip_rows", 0) |
| 349 | + ds_sheet = _resolve_sheet_name(wb, self.config.get("dataset_sheet", DEFAULT_DATASET_SHEET)) |
| 350 | + dist_sheet = _resolve_sheet_name(wb, self.config.get("distribution_sheet", DEFAULT_DISTRIBUTION_SHEET)) |
314 | 351 |
|
315 | 352 | ds_rows = dist_rows = None |
316 | 353 | errors = [] |
@@ -578,6 +615,27 @@ def import_stage(self, harvest_object): |
578 | 615 | model.Session.commit() |
579 | 616 | return True |
580 | 617 |
|
| 618 | + except sa.exc.IntegrityError as e: |
| 619 | + model.Session.rollback() |
| 620 | + orig = getattr(e, "orig", None) |
| 621 | + orig_type = type(orig).__name__ if orig else "" |
| 622 | + if orig_type == "UniqueViolation" and "resource_pkey" in str(e): |
| 623 | + resources = package_dict.get("resources", []) |
| 624 | + ids = [r.get("id") for r in resources if r.get("id")] |
| 625 | + dupes = sorted({i for i in ids if ids.count(i) > 1}) |
| 626 | + self._save_object_error( |
| 627 | + "IDs de distribución duplicados en el dataset '%s': %s. " |
| 628 | + "Revisá que los distribution_identifier sean únicos en la fuente." |
| 629 | + % (package_dict.get("name", "?"), ", ".join(dupes)), |
| 630 | + harvest_object, |
| 631 | + "Import", |
| 632 | + ) |
| 633 | + else: |
| 634 | + self._save_object_error( |
| 635 | + "Error de integridad al guardar en CKAN: %s" % str(e), |
| 636 | + harvest_object, |
| 637 | + "Import", |
| 638 | + ) |
581 | 639 | except Exception as e: |
582 | 640 | model.Session.rollback() |
583 | 641 | self._save_object_error( |
|
0 commit comments