core-lens 0.1.dev183__tar.gz → 0.1.dev186__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/PKG-INFO +2 -2
  2. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/SKILLS.md +38 -10
  3. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/_version.py +2 -2
  4. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/aoi.py +4 -2
  5. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/base/entity.py +2 -2
  6. core_lens-0.1.dev186/src/core_lens/entities/farms.py +63 -0
  7. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/utils/polars_utils.py +28 -1
  8. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/utils/spatial.py +3 -2
  9. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  10. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  11. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  12. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.github/pull_request_template.md +0 -0
  13. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.github/workflows/ci.yml +0 -0
  14. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.github/workflows/gh-pages.yml +0 -0
  15. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.github/workflows/pre-release.yml +0 -0
  16. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.github/workflows/release.yml +0 -0
  17. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.gitignore +0 -0
  18. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.gitmessage +0 -0
  19. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.pre-commit-config.yaml +0 -0
  20. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/.python-version +0 -0
  21. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/CONTRIBUTING.md +0 -0
  22. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/LICENSE +0 -0
  23. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/README.md +0 -0
  24. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/README.md +0 -0
  25. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/bench_aoi.py +0 -0
  26. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/bench_entity.py +0 -0
  27. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/bench_export.py +0 -0
  28. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/bench_polars_utils.py +0 -0
  29. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/bench_result.py +0 -0
  30. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/bench_schema.py +0 -0
  31. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/bench_season.py +0 -0
  32. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/bench_spatial.py +0 -0
  33. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/bench_view.py +0 -0
  34. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/benchmarks/run_all.sh +0 -0
  35. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/Makefile +0 -0
  36. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/make.bat +0 -0
  37. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/concepts.md +0 -0
  38. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/conf.py +0 -0
  39. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/export.md +0 -0
  40. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/index.rst +0 -0
  41. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/intro.md +0 -0
  42. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/logging.md +0 -0
  43. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/plots.md +0 -0
  44. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/plugins.md +0 -0
  45. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/queries.md +0 -0
  46. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/quickstart.md +0 -0
  47. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/docs/source/stats.md +0 -0
  48. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/examples/demo_mws.py +0 -0
  49. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/examples/demo_tehsil.py +0 -0
  50. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/hooks/mypy.sh +0 -0
  51. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/hooks/no-parquet-outside-fixtures.sh +0 -0
  52. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/hooks/pytest.sh +0 -0
  53. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/pyproject.toml +0 -0
  54. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/__init__.py +0 -0
  55. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/__main__.py +0 -0
  56. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/base/__init__.py +0 -0
  57. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/base/namespaces/__init__.py +0 -0
  58. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/base/namespaces/plot.py +0 -0
  59. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/base/namespaces/stats.py +0 -0
  60. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/base/result.py +0 -0
  61. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/base/view.py +0 -0
  62. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/entities/__init__.py +0 -0
  63. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/entities/mws.py +0 -0
  64. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/entities/tehsil.py +0 -0
  65. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/entities/waterbody.py +0 -0
  66. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/export/__init__.py +0 -0
  67. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/export/formats.py +0 -0
  68. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/py.typed +0 -0
  69. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/schema/__init__.py +0 -0
  70. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/schema/detection.py +0 -0
  71. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/schema/profile.py +0 -0
  72. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/utils/__init__.py +0 -0
  73. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/utils/paths.py +0 -0
  74. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/src/core_lens/utils/season.py +0 -0
  75. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/fixtures/generate_fixtures.py +0 -0
  76. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/conftest.py +0 -0
  77. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_aoi.py +0 -0
  78. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_entities.py +0 -0
  79. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_entity.py +0 -0
  80. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_export.py +0 -0
  81. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_main.py +0 -0
  82. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_plot.py +0 -0
  83. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_polars_utils.py +0 -0
  84. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_profile.py +0 -0
  85. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_result.py +0 -0
  86. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_schema_detection.py +0 -0
  87. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_schema_profile.py +0 -0
  88. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_season.py +0 -0
  89. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_season_config.py +0 -0
  90. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_spatial.py +0 -0
  91. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_stats.py +0 -0
  92. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/tests/unit/test_view.py +0 -0
  93. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/usage.md +0 -0
  94. {core_lens-0.1.dev183 → core_lens-0.1.dev186}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: core-lens
3
- Version: 0.1.dev183
3
+ Version: 0.1.dev186
4
4
  Summary: Query, analyse, and visualise CoreStack's microwatershed and Earth science data through a clean, composable Python API.
5
5
  Project-URL: Homepage, https://github.com/ApoorvaKashyap/core-lens
6
6
  Project-URL: Issues, https://github.com/ApoorvaKashyap/core-lens/issues
@@ -57,7 +57,9 @@ aoi = AoI(DATA_ROOT, bbox=(76.0, 31.0, 78.0, 33.0))
57
57
  aoi = AoI(DATA_ROOT, mws_id="13_551")
58
58
 
59
59
  # Optional: Override default seasons (kharif, rabi, zaid)
60
- custom_seasons = SeasonConfig(kharif=("06-01", "10-15"), rabi=("10-16", "02-28"), zaid=("03-01", "05-31"))
60
+ custom_seasons = SeasonConfig(
61
+ kharif=("06-01", "10-15"), rabi=("10-16", "02-28"), zaid=("03-01", "05-31")
62
+ )
61
63
  aoi_custom = AoI(DATA_ROOT, tehsil="Pangi", seasons=custom_seasons)
62
64
  ```
63
65
 
@@ -78,6 +80,7 @@ spatial_filtered = mws_view.spatial_filter(bbox=(76.5, 31.5, 77.5, 32.5))
78
80
  # Temporal Filtering
79
81
  # Note: You can filter by exact years, a range of years, or predefined seasons.
80
82
  from core_lens.base.view import Season
83
+
81
84
  temporal_view = mws_view.between(year=(2020, 2022), season=Season.KHARIF)
82
85
  current_view = mws_view.between(season=Season.CURRENT)
83
86
  ```
@@ -98,9 +101,9 @@ annual_result = temporal_view.annual.materialise()
98
101
 
99
102
  ```python
100
103
  # To access underlying data structures:
101
- df = static_result.df() # Polars DataFrame (Zero-copy)
102
- lazy_df = static_result.lazy() # Polars LazyFrame
103
- gdf = static_result.gdf() # GeoPandas GeoDataFrame (Heavy!)
104
+ df = static_result.df() # Polars DataFrame (Zero-copy)
105
+ lazy_df = static_result.lazy() # Polars LazyFrame
106
+ gdf = static_result.gdf() # GeoPandas GeoDataFrame (Heavy!)
104
107
  ```
105
108
 
106
109
  ## 5. Computation and Data Manipulation (Result API)
@@ -145,30 +148,53 @@ result.stats.describe(columns=["ndvi", "rainfall"])
145
148
 
146
149
  # Correlation (pearson, spearman, kendall)
147
150
  from core_lens.base.namespaces.stats import CorrelateMethod
148
- result.stats.correlate(columns=["ndvi", "rainfall"], method=CorrelateMethod.PEARSON, across="entity")
151
+
152
+ result.stats.correlate(
153
+ columns=["ndvi", "rainfall"], method=CorrelateMethod.PEARSON, across="entity"
154
+ )
149
155
 
150
156
  # Hypothesis Testing (t-test, mann-whitney, wilcoxon, ks, chi-square)
151
157
  from core_lens.base.namespaces.stats import TestMethod
152
- result.stats.test(column="cropping_intensity", groups="temperature_zone", method=TestMethod.MANN_WHITNEY)
158
+
159
+ result.stats.test(
160
+ column="cropping_intensity",
161
+ groups="temperature_zone",
162
+ method=TestMethod.MANN_WHITNEY,
163
+ )
153
164
 
154
165
  # Change Detection (absolute, percentage, trend)
155
166
  from core_lens.base.namespaces.stats import ChangeMethod
156
- result.stats.change(column="tree_cover", from_period=2018, to_period=2023, method=ChangeMethod.PERCENTAGE)
167
+
168
+ result.stats.change(
169
+ column="tree_cover",
170
+ from_period=2018,
171
+ to_period=2023,
172
+ method=ChangeMethod.PERCENTAGE,
173
+ )
157
174
 
158
175
  # Anomaly Detection
159
176
  from core_lens.base.namespaces.stats import AnomalyTsMethod, AnomalyCrossMethod
177
+
160
178
  # Mode 1: Cross-sectional (zscore, iqr, percentile, threshold)
161
- result.stats.anomaly(column="ndvi", mode="cross_sectional", method=AnomalyCrossMethod.ZSCORE, baseline=(2010, 2020))
179
+ result.stats.anomaly(
180
+ column="ndvi",
181
+ mode="cross_sectional",
182
+ method=AnomalyCrossMethod.ZSCORE,
183
+ baseline=(2010, 2020),
184
+ )
162
185
  # Mode 2: Time-series (stl, cusum, mad)
163
- result.stats.anomaly(column="ndvi", mode="timeseries", method=AnomalyTsMethod.STL, baseline=(2010, 2018))
186
+ result.stats.anomaly(
187
+ column="ndvi", mode="timeseries", method=AnomalyTsMethod.STL, baseline=(2010, 2018)
188
+ )
164
189
 
165
190
  # Similarity Matching (euclidean, cosine, mahalanobis, manhattan)
166
191
  from core_lens.base.namespaces.stats import SimilarityMethod
192
+
167
193
  result.stats.similarity(
168
194
  target="13_551",
169
195
  columns={"rainfall": ("annual", {"year": 2018})},
170
196
  method=SimilarityMethod.EUCLIDEAN,
171
- top_n=10
197
+ top_n=10,
172
198
  )
173
199
  ```
174
200
 
@@ -214,6 +240,7 @@ You can extend core-lens with custom entities by subclassing `BaseEntity`.
214
240
  ```python
215
241
  from core_lens.base import BaseEntity
216
242
 
243
+
217
244
  class CustomEntity(BaseEntity):
218
245
  @property
219
246
  def key_cols(self) -> list[str]:
@@ -231,6 +258,7 @@ class CustomEntity(BaseEntity):
231
258
  def annual_path(self) -> str | None:
232
259
  return "custom/annual.parquet"
233
260
 
261
+
234
262
  AoI.register(CustomEntity)
235
263
  ```
236
264
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.dev183'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev183')
21
+ __version__ = version = '0.1.dev186'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev186')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -20,7 +20,7 @@ from loguru import logger
20
20
 
21
21
  from core_lens.base.entity import BaseEntity, EntityValidationError
22
22
  from core_lens.utils.paths import is_cloud_uri
23
- from core_lens.utils.polars_utils import cached_read_schema
23
+ from core_lens.utils.polars_utils import cached_read_schema, parquet_scan_path
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from core_lens.base.view import View
@@ -225,7 +225,9 @@ def _cached_resolve_boundary(
225
225
  geom_col = schema.geometry_col
226
226
  static_path = candidate._resolve(candidate.static_path)
227
227
 
228
- lf = pl.scan_parquet(static_path, storage_options=storage_options or None)
228
+ lf = pl.scan_parquet(
229
+ parquet_scan_path(static_path), storage_options=storage_options or None
230
+ )
229
231
  filter_expr = pl.lit(True)
230
232
  schema_types = cached_read_schema(static_path, storage_options)
231
233
  for col, val in entity_kwargs.items():
@@ -29,7 +29,7 @@ from core_lens.utils.spatial import (
29
29
  build_bbox_index,
30
30
  exact_spatial_filter,
31
31
  )
32
- from core_lens.utils.polars_utils import cached_read_schema
32
+ from core_lens.utils.polars_utils import cached_read_schema, parquet_scan_path
33
33
 
34
34
  if TYPE_CHECKING:
35
35
  import shapely
@@ -475,7 +475,7 @@ class BaseEntity(ABC):
475
475
  )
476
476
 
477
477
  # --- Combine attribute and spatial filters into a single lazy pipeline ---
478
- lf = pl.scan_parquet(static)
478
+ lf = pl.scan_parquet(parquet_scan_path(static))
479
479
 
480
480
  if attr_kwargs:
481
481
  filter_expr = pl.lit(True)
@@ -0,0 +1,63 @@
1
+ from core_lens.base import BaseEntity
2
+
3
+
4
+ class FarmEntity(BaseEntity):
5
+ """Farms entity.
6
+
7
+ Backed by:
8
+ - Static: farms/static
9
+ - Annual: farms/annual
10
+ - SubAnnual: farms/sub_annual
11
+
12
+ All paths are relative to the AoI ``data_root``.
13
+ """
14
+
15
+ @property
16
+ def key_cols(self) -> list[str]:
17
+ """Get the property value.
18
+
19
+ Returns:
20
+ list[str]: The key columns for the entity.
21
+
22
+ """
23
+ return ["farm_id"]
24
+
25
+ @property
26
+ def geometry_col(self) -> str:
27
+ """Get the property value.
28
+
29
+ Returns:
30
+ str: The geometry column name.
31
+
32
+ """
33
+ return "geometry"
34
+
35
+ @property
36
+ def static_path(self) -> str:
37
+ """Get the property value.
38
+
39
+ Returns:
40
+ str: The relative path to the static data.
41
+
42
+ """
43
+ return "farms/static.parquet"
44
+
45
+ @property
46
+ def annual_path(self) -> str:
47
+ """Get the property value.
48
+
49
+ Returns:
50
+ str: The relative path to the annual data.
51
+
52
+ """
53
+ return "farms/annual.parquet"
54
+
55
+ @property
56
+ def sub_annual_path(self) -> str:
57
+ """Get the property value.
58
+
59
+ Returns:
60
+ str: The relative path to the monthly data.
61
+
62
+ """
63
+ return "farms/sub_annual.parquet"
@@ -7,6 +7,8 @@ import functools
7
7
 
8
8
  import polars as pl
9
9
 
10
+ import os
11
+
10
12
  _GPU_AVAILABLE: bool | None = None # None = not yet probed
11
13
 
12
14
 
@@ -16,9 +18,34 @@ def _so_key(storage_options: dict[str, Any] | None) -> tuple[tuple[str, Any], ..
16
18
  return tuple(sorted(storage_options.items()))
17
19
 
18
20
 
21
+ def parquet_scan_path(path: str) -> str:
22
+ """Return a glob-safe path for ``pl.scan_parquet``.
23
+
24
+ When *path* is a directory polars will reject it if the directory contains
25
+ files with mixed extensions (e.g. a ``data_dictionary.csv`` sitting
26
+ alongside Hive-partitioned ``.parquet`` files). This helper coerces a bare
27
+ directory to ``<path>/**/*.parquet`` so only Parquet files are matched.
28
+
29
+ Args:
30
+ path (str): Filesystem path or cloud URI to a Parquet file or directory.
31
+
32
+ Returns:
33
+ str: The original path if it already points to a file, otherwise
34
+ ``<path>/**/*.parquet``.
35
+ """
36
+ if os.path.isdir(path):
37
+ return os.path.join(path, "**", "*.parquet")
38
+ return path
39
+
40
+
19
41
  @functools.cache
20
42
  def _cached_schema_internal(path: str, so_key: tuple[tuple[str, Any], ...]) -> Any:
21
- return pl.scan_parquet(path, storage_options=dict(so_key) or None).collect_schema()
43
+ # Coerce bare directory paths to a *.parquet glob so polars doesn't raise
44
+ # InvalidOperationError when the directory contains mixed-extension files
45
+ # (e.g. data_dictionary.csv alongside Hive-partitioned .parquet files).
46
+ return pl.scan_parquet(
47
+ parquet_scan_path(path), storage_options=dict(so_key) or None
48
+ ).collect_schema()
22
49
 
23
50
 
24
51
  def cached_read_schema(path: str, storage_options: dict[str, Any] | None = None) -> Any:
@@ -12,6 +12,7 @@ import shapely
12
12
  from loguru import logger
13
13
 
14
14
  from core_lens.utils.paths import is_cloud_uri, path_exists, resolve_fs_and_path
15
+ from core_lens.utils.polars_utils import parquet_scan_path
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  pass
@@ -361,7 +362,7 @@ def exact_spatial_filter(
361
362
 
362
363
  # Build a lazy scan and push down an inner join to load only required geometries.
363
364
  full_df = (
364
- pl.scan_parquet(static_path)
365
+ pl.scan_parquet(parquet_scan_path(static_path))
365
366
  .select(key_cols + [geometry_col])
366
367
  .join(candidates.select(key_cols).lazy(), on=key_cols, how="semi")
367
368
  .collect()
@@ -684,7 +685,7 @@ def point_in_entities(
684
685
  static_path: str = entity._resolve(entity.static_path)
685
686
 
686
687
  geom_df = (
687
- pl.scan_parquet(static_path)
688
+ pl.scan_parquet(parquet_scan_path(static_path))
688
689
  .select(key_cols + [geom_col])
689
690
  .join(candidates.select(key_cols).lazy(), on=key_cols, how="semi")
690
691
  .collect()
File without changes
File without changes
File without changes
File without changes