-
Notifications
You must be signed in to change notification settings - Fork 11
FiboaBaseConverter: bounded drops, and schemas up front #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,14 +10,109 @@ class FiboaBaseConverter(BaseConverter): | |
| area_is_in_ha = True | ||
| area_calculate_missing = False | ||
| use_variant_as_determination = False | ||
| # rows that cannot validate are dropped up to this share of the file, above | ||
| # which the conversion fails: a handful of bad rows in a source is normal, | ||
| # a broken mapping is not. Raise it for a source that is genuinely that | ||
| # patchy, and the message says how many rows it would have dropped. | ||
| max_dropped_share = 0.01 | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| super().__init__(*args, **kwargs) | ||
| self.extensions.add(get_fiboa_uri()) | ||
| if self.use_variant_as_determination: | ||
| # The column is added in post_migrate; list it so it survives the | ||
| # "remove unlisted columns" step of the base converter. | ||
| self.columns = {**self.columns, "determination:datetime": "determination:datetime"} | ||
|
|
||
| def convert(self, *args, **kwargs): | ||
| self._prewarm_schemas() | ||
| return super().convert(*args, **kwargs) | ||
|
|
||
| def _prewarm_schemas(self): | ||
| """Fetch every schema this conversion will need before doing any real work. | ||
|
|
||
| The schema hosts (vecorel.org, fiboa.org) fail intermittently, and | ||
| without this a blip after a long source download killed the conversion | ||
| at its very last step. load_file caches per process, so a successful | ||
| pre-warm makes the write network-free — and tells the converter what | ||
| every row must carry (see _required_properties).""" | ||
| import time | ||
|
|
||
| from vecorel_cli.vecorel.util import load_file | ||
| from vecorel_cli.vecorel.version import vecorel_version | ||
|
|
||
| uris = set(self.extensions) | ||
| uris.add(get_fiboa_uri()) | ||
| uris.add(f"https://vecorel.org/specification/v{vecorel_version}/schema.yaml") | ||
| # Nothing has been downloaded or converted yet, so a schema that cannot | ||
| # be fetched should say so now rather than after a wait: one retry for a | ||
| # dropped connection, then out. | ||
| attempts = 2 | ||
| for uri in sorted(uris): | ||
| for attempt in range(attempts): | ||
| try: | ||
| load_file(uri) | ||
| break | ||
| except Exception as e: | ||
| if attempt == attempts - 1: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After a conversion retries make sense to me, but with prewarm I'd rather want the process to fail immediately instead of waiting 4mins, I think? |
||
| raise RuntimeError( | ||
| f"Cannot load schema {uri} after {attempts} attempts: {e}" | ||
| ) from e | ||
| self.warning(f"Schema fetch failed ({uri}), retrying: {str(e)[:100]}") | ||
| time.sleep(2) | ||
|
|
||
| def _required_properties(self) -> set[str]: | ||
| """What the schemas this conversion declares require of every row. | ||
|
|
||
| A converter should not have to list them: the core schema requires id | ||
| and geometry, the crop extension crop:code and crop:code_list, and a | ||
| converter that declares an extension takes on its rules with it. The | ||
| schemas are already in memory, fetched by _prewarm_schemas. | ||
| """ | ||
| try: | ||
| schema = self.create_collection(self.id).merge_schemas() | ||
| except Exception as e: | ||
| self.warning(f"Cannot resolve the declared schemas ({e}); requiring an id only") | ||
| return {"id"} | ||
| return set(schema.get("required", [])) | ||
|
|
||
| def post_migrate(self, gdf): | ||
| gdf = super().post_migrate(gdf) | ||
|
|
||
| # post_migrate runs before columns are renamed, so look up the source column | ||
| for key in sorted(self._required_properties()): | ||
| for src, dst in self.columns.items(): | ||
| targets = dst if isinstance(dst, (list, tuple)) else [dst] | ||
| if key in targets and src in gdf.columns: | ||
| nulls = gdf[src].isna() | ||
| if nulls.any(): | ||
| share = nulls.mean() | ||
| if share > self.max_dropped_share: | ||
| raise ValueError( | ||
| f"{int(nulls.sum())} of {len(gdf)} rows ({share:.1%}) have no " | ||
| f"{key} ({src}); fix the converter instead of dropping them" | ||
| ) | ||
| self.warning( | ||
| f"Dropping {int(nulls.sum())} rows without a value for {key} ({src})" | ||
| ) | ||
| gdf = gdf[~nulls] | ||
|
|
||
| # A null or empty geometry cannot be validated, tiled or Hilbert-sorted, | ||
| # and fails the run at its very last step, so it falls under the same | ||
| # bounded rule as the required properties. | ||
| if gdf.active_geometry_name is not None: | ||
| geom = gdf.geometry | ||
| blank = geom.isna() | geom.is_empty | ||
| if blank.any(): | ||
| share = blank.mean() | ||
| if share > self.max_dropped_share: | ||
| raise ValueError( | ||
| f"{int(blank.sum())} of {len(gdf)} rows ({share:.1%}) have an empty or " | ||
| f"missing geometry; fix the converter instead of dropping them" | ||
| ) | ||
| self.warning(f"Dropping {int(blank.sum())} rows with an empty or missing geometry") | ||
| gdf = gdf[~blank] | ||
|
|
||
| gdf_area_key = next((k for k, v in self.columns.items() if v == AREA_KEY), None) | ||
| if self.area_calculate_missing: | ||
| # If CRS is not in meters, reproject to an equal-area projection for area calculation | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty arbitrary number. I feel like we should decide for a consistent behavior (all or none). Maybe leave choice to users. Depending on the usecase you may want different behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is some check that increases quality. Don't raise problems if the error margin is low, but set some default threshold.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like from a scientific perspective just ignoring errors when the error number is low is the wrong approach and doesn't necessarily increases quality. It depends on the usecase. We should probably discuss this further how we want to go ahead with such cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See vecorel/cli#33