core-lens 0.1.dev114__tar.gz → 0.1.dev120__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.
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/PKG-INFO +8 -1
- core_lens-0.1.dev120/docs/source/export.md +47 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/source/index.rst +2 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/source/intro.md +2 -1
- core_lens-0.1.dev120/docs/source/logging.md +32 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/source/plots.md +1 -1
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/pyproject.toml +6 -1
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/__init__.py +4 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/aoi.py +66 -16
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/base/entity.py +54 -18
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/base/namespaces/plot.py +25 -25
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/base/namespaces/stats.py +25 -25
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/base/result.py +16 -10
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/base/view.py +52 -18
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/entities/mws.py +15 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/entities/tehsil.py +12 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/entities/waterbody.py +12 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/export/formats.py +15 -15
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/schema/detection.py +6 -6
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/schema/profile.py +2 -2
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/utils/polars_utils.py +10 -7
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/utils/season.py +52 -22
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/utils/spatial.py +58 -28
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_entity.py +73 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_plot.py +35 -0
- core_lens-0.1.dev120/tests/unit/test_polars_utils.py +87 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_spatial.py +104 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_stats.py +219 -11
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_view.py +76 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/uv.lock +251 -251
- core_lens-0.1.dev114/tests/unit/test_polars_utils.py +0 -14
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/.gitignore +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/.gitmessage +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/.python-version +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/LICENSE +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/README.md +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/SKILLS.md +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/Makefile +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/make.bat +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/source/queries.md +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/docs/source/stats.md +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev114 → core_lens-0.1.dev120}/tests/unit/test_season_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-lens
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.dev120
|
|
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
|
|
@@ -17,6 +17,7 @@ Requires-Python: >=3.13
|
|
|
17
17
|
Requires-Dist: core-lens[core]
|
|
18
18
|
Requires-Dist: core-lens[spatial]
|
|
19
19
|
Provides-Extra: core
|
|
20
|
+
Requires-Dist: loguru>=0.7.3; extra == 'core'
|
|
20
21
|
Requires-Dist: polars<2,>=1.39.0; extra == 'core'
|
|
21
22
|
Requires-Dist: pyarrow<25,>=23.0.0; extra == 'core'
|
|
22
23
|
Requires-Dist: pydantic<3,>=2.13.3; extra == 'core'
|
|
@@ -25,6 +26,7 @@ Requires-Dist: bokeh<4,>=3.6.0; extra == 'full'
|
|
|
25
26
|
Requires-Dist: duckdb<2,>=1.5.3; extra == 'full'
|
|
26
27
|
Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'full'
|
|
27
28
|
Requires-Dist: geopandas<2,>=1.1.3; extra == 'full'
|
|
29
|
+
Requires-Dist: loguru>=0.7.3; extra == 'full'
|
|
28
30
|
Requires-Dist: lonboard<1,>=0.16.0; extra == 'full'
|
|
29
31
|
Requires-Dist: matplotlib<4,>=3.10.9; extra == 'full'
|
|
30
32
|
Requires-Dist: polars<2,>=1.39.0; extra == 'full'
|
|
@@ -41,6 +43,7 @@ Requires-Dist: cudf-polars-cu13==26.6.*; extra == 'full-gpu'
|
|
|
41
43
|
Requires-Dist: duckdb<2,>=1.5.3; extra == 'full-gpu'
|
|
42
44
|
Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'full-gpu'
|
|
43
45
|
Requires-Dist: geopandas<2,>=1.1.3; extra == 'full-gpu'
|
|
46
|
+
Requires-Dist: loguru>=0.7.3; extra == 'full-gpu'
|
|
44
47
|
Requires-Dist: lonboard<1,>=0.16.0; extra == 'full-gpu'
|
|
45
48
|
Requires-Dist: matplotlib<4,>=3.10.9; extra == 'full-gpu'
|
|
46
49
|
Requires-Dist: polars<2,>=1.39.0; extra == 'full-gpu'
|
|
@@ -53,18 +56,21 @@ Requires-Dist: statsmodels<1,>=0.14.6; extra == 'full-gpu'
|
|
|
53
56
|
Provides-Extra: gpu
|
|
54
57
|
Requires-Dist: cudf-cu13==26.6.*; extra == 'gpu'
|
|
55
58
|
Requires-Dist: cudf-polars-cu13==26.6.*; extra == 'gpu'
|
|
59
|
+
Requires-Dist: loguru>=0.7.3; extra == 'gpu'
|
|
56
60
|
Requires-Dist: polars<2,>=1.39.0; extra == 'gpu'
|
|
57
61
|
Requires-Dist: pyarrow<25,>=23.0.0; extra == 'gpu'
|
|
58
62
|
Requires-Dist: pydantic<3,>=2.13.3; extra == 'gpu'
|
|
59
63
|
Provides-Extra: spatial
|
|
60
64
|
Requires-Dist: duckdb<2,>=1.5.3; extra == 'spatial'
|
|
61
65
|
Requires-Dist: geopandas<2,>=1.1.3; extra == 'spatial'
|
|
66
|
+
Requires-Dist: loguru>=0.7.3; extra == 'spatial'
|
|
62
67
|
Requires-Dist: polars<2,>=1.39.0; extra == 'spatial'
|
|
63
68
|
Requires-Dist: pyarrow<25,>=23.0.0; extra == 'spatial'
|
|
64
69
|
Requires-Dist: pydantic<3,>=2.13.3; extra == 'spatial'
|
|
65
70
|
Requires-Dist: pyproj<4,>=3.7.2; extra == 'spatial'
|
|
66
71
|
Requires-Dist: shapely<3,>=2.1.2; extra == 'spatial'
|
|
67
72
|
Provides-Extra: stats
|
|
73
|
+
Requires-Dist: loguru>=0.7.3; extra == 'stats'
|
|
68
74
|
Requires-Dist: polars<2,>=1.39.0; extra == 'stats'
|
|
69
75
|
Requires-Dist: pyarrow<25,>=23.0.0; extra == 'stats'
|
|
70
76
|
Requires-Dist: pydantic<3,>=2.13.3; extra == 'stats'
|
|
@@ -73,6 +79,7 @@ Requires-Dist: statsmodels<1,>=0.14.6; extra == 'stats'
|
|
|
73
79
|
Provides-Extra: viz
|
|
74
80
|
Requires-Dist: bokeh<4,>=3.6.0; extra == 'viz'
|
|
75
81
|
Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'viz'
|
|
82
|
+
Requires-Dist: loguru>=0.7.3; extra == 'viz'
|
|
76
83
|
Requires-Dist: lonboard<1,>=0.16.0; extra == 'viz'
|
|
77
84
|
Requires-Dist: matplotlib<4,>=3.10.9; extra == 'viz'
|
|
78
85
|
Requires-Dist: polars<2,>=1.39.0; extra == 'viz'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Data Export
|
|
2
|
+
|
|
3
|
+
CoreLens provides a unified `core_lens.export` module for saving `Result` objects to various file formats. Standard tabular formats use Polars for high-speed writes, while geospatial formats leverage DuckDB's spatial extensions for robust geometry encoding.
|
|
4
|
+
|
|
5
|
+
## Standard Formats
|
|
6
|
+
|
|
7
|
+
Export non-spatial results to standard formats. Note that these formats cannot encode geometry columns; if your `Result` has geometry, these functions will raise a `TypeError`.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from core_lens.export import parquet, csv, json
|
|
11
|
+
|
|
12
|
+
res = aoi.mws.annual
|
|
13
|
+
|
|
14
|
+
# Export to Parquet with ZSTD compression
|
|
15
|
+
parquet(res, "output.parquet", compression="zstd")
|
|
16
|
+
|
|
17
|
+
# Export to CSV
|
|
18
|
+
csv(res, "output.csv", separator=",")
|
|
19
|
+
|
|
20
|
+
# Export to JSON
|
|
21
|
+
json(res, "output.json", pretty=True)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Geospatial Formats
|
|
25
|
+
|
|
26
|
+
Export spatial data to GeoParquet and GeoJSON. The result must have geometry attached (e.g., by calling `.with_geometry()`).
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from core_lens.export import geoparquet, geojson
|
|
30
|
+
|
|
31
|
+
# Make sure geometry is attached!
|
|
32
|
+
spatial_res = aoi.mws.annual.with_geometry()
|
|
33
|
+
|
|
34
|
+
# Export to GeoParquet with DuckDB COPY options (e.g., partitioning)
|
|
35
|
+
geoparquet(
|
|
36
|
+
spatial_res,
|
|
37
|
+
"output.geoparquet",
|
|
38
|
+
compression="ZSTD",
|
|
39
|
+
partition_by="year"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Export to GeoJSON
|
|
43
|
+
geojson(spatial_res, "output.json")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> [!NOTE]
|
|
47
|
+
> Exporting to geospatial formats automatically uses DuckDB to cast WKB geometries to proper spatial types via the `spatial` extension. CoreLens handles the installation and loading of the extension seamlessly under the hood.
|
|
@@ -12,4 +12,5 @@ CoreLens provides a unified interface over microwatersheds, administrative bound
|
|
|
12
12
|
- **Pluggable Entities**: Built-in support for standard units (MWS, Tehsil) with a simple plugin architecture for adding new domain entities.
|
|
13
13
|
- **Temporal & Seasonal Awareness**: Native support for agronomic seasons (Kharif, Rabi, Zaid) and time-range filtering.
|
|
14
14
|
- **Spatial Statistics & Analysis**: Built-in methods for anomaly detection, spatial similarity, temporal correlation, and hypothesis testing.
|
|
15
|
-
- **Interactive Visualisation**: Generate interactive maps using Lonboard and timeseries/scatter plots using Bokeh
|
|
15
|
+
- **Interactive Visualisation**: Generate interactive maps using Lonboard with zero-copy GeoArrow rendering directly from Polars, and timeseries/scatter plots using Bokeh.
|
|
16
|
+
- **High-Performance Exports**: Export results directly to Parquet, JSON, CSV, or spatially-enabled GeoParquet and GeoJSON using integrated DuckDB spatial extensions.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Logging
|
|
2
|
+
|
|
3
|
+
`core_lens` uses [Loguru](https://loguru.readthedocs.io/) for internal logging and tracing.
|
|
4
|
+
|
|
5
|
+
To avoid polluting the standard output of your downstream applications, the logger is disabled by default upon import.
|
|
6
|
+
|
|
7
|
+
## Enabling Logs
|
|
8
|
+
|
|
9
|
+
If you want to view the internal tracing logs (useful for debugging spatial resolution issues, inspecting generated time filters, or tracing validation errors), you need to explicitly enable the `core_lens` logger in your application and add a handler (e.g., `sys.stdout` or a log file).
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import sys
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from core_lens import AoI
|
|
15
|
+
|
|
16
|
+
# 1. Enable the internal logger
|
|
17
|
+
logger.enable("core_lens")
|
|
18
|
+
|
|
19
|
+
# 2. Add a handler with your desired level and formatting
|
|
20
|
+
logger.add(sys.stdout, level="DEBUG")
|
|
21
|
+
|
|
22
|
+
# 3. Operations will now emit tracing logs
|
|
23
|
+
aoi = AoI("data/", tehsil="Pangi", district="Chamba")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Log Levels
|
|
27
|
+
|
|
28
|
+
`core_lens` emits logs at different levels depending on the operational context:
|
|
29
|
+
|
|
30
|
+
- **INFO**: Materialisation events (reading static, annual, or fortnightly Parquet files), entity registrations, AoI initialisation, and cross-entity spatial joins.
|
|
31
|
+
- **DEBUG**: Sub-operations such as spatial bounding box index generation, exact geometry relationships, lazy attribute filters, and query generation.
|
|
32
|
+
- **ERROR**: Incorrect initialisation arguments, failed path validations, missing geometries, unknown schema structures, or conflicting date configurations.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Visualisation and Plots
|
|
2
2
|
|
|
3
|
-
The `result.plot` namespace provides integrated plotting capabilities. Spatial plots use **Lonboard** for high-performance interactive maps, while charts and timeseries use **Bokeh**.
|
|
3
|
+
The `result.plot` namespace provides integrated plotting capabilities. Spatial plots use **Lonboard** for high-performance interactive maps (achieved via zero-copy GeoArrow memory mapping directly from Polars, bypassing GeoPandas), while charts and timeseries use **Bokeh**.
|
|
4
4
|
|
|
5
5
|
Plot methods return plot objects (like a Bokeh Figure or Lonboard Map) which can be displayed using `bokeh.io.show` or exported via `bokeh.io.save`.
|
|
6
6
|
|
|
@@ -44,7 +44,12 @@ venvPath = "."
|
|
|
44
44
|
venv = ".venv"
|
|
45
45
|
|
|
46
46
|
[project.optional-dependencies]
|
|
47
|
-
core = [
|
|
47
|
+
core = [
|
|
48
|
+
"polars>=1.39.0,<2",
|
|
49
|
+
"pyarrow>=23.0.0,<25",
|
|
50
|
+
"pydantic>=2.13.3,<3",
|
|
51
|
+
"loguru>=0.7.3",
|
|
52
|
+
]
|
|
48
53
|
spatial = [
|
|
49
54
|
"core-lens[core]",
|
|
50
55
|
"duckdb>=1.5.3,<2",
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, '
|
|
21
|
+
__version__ = version = '0.1.dev120'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev120')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
|
|
|
12
12
|
from core_lens.base.result import Result
|
|
13
13
|
|
|
14
14
|
import polars as pl
|
|
15
|
+
from loguru import logger
|
|
15
16
|
|
|
16
17
|
from core_lens.base.entity import BaseEntity, EntityValidationError
|
|
17
18
|
|
|
@@ -67,10 +68,10 @@ class SeasonConfig:
|
|
|
67
68
|
"""Return the season name for a given date.
|
|
68
69
|
|
|
69
70
|
Args:
|
|
70
|
-
d: The date to classify.
|
|
71
|
+
d (date): The date to classify.
|
|
71
72
|
|
|
72
73
|
Returns:
|
|
73
|
-
``"kharif"``, ``"rabi"``, or ``"zaid"``.
|
|
74
|
+
str: ``"kharif"``, ``"rabi"``, or ``"zaid"``.
|
|
74
75
|
"""
|
|
75
76
|
md = f"{d.month:02d}-{d.day:02d}"
|
|
76
77
|
for name in ("kharif", "rabi", "zaid"):
|
|
@@ -155,16 +156,16 @@ class AoI:
|
|
|
155
156
|
"""Resolve the AoI boundary and scope all registered entities.
|
|
156
157
|
|
|
157
158
|
Args:
|
|
158
|
-
data_root: Path to the root data directory. Prepended to every
|
|
159
|
+
data_root (str): Path to the root data directory. Prepended to every
|
|
159
160
|
relative entity path that does not start with ``/`` or a URI
|
|
160
161
|
scheme.
|
|
161
|
-
bbox: Bounding box as ``(minx, miny, maxx, maxy)`` in WGS-84.
|
|
162
|
+
bbox (tuple[float, float, float, float] | None, optional): Bounding box as ``(minx, miny, maxx, maxy)`` in WGS-84.
|
|
162
163
|
Mutually exclusive with ``geometry`` and ``entity_kwargs``.
|
|
163
|
-
geometry: Arbitrary Shapely geometry. Used as-is. Mutually
|
|
164
|
+
geometry (shapely.Geometry | None, optional): Arbitrary Shapely geometry. Used as-is. Mutually
|
|
164
165
|
exclusive with ``bbox`` and ``entity_kwargs``.
|
|
165
|
-
seasons: :class:`SeasonConfig` override. Defaults to the library
|
|
166
|
+
seasons (SeasonConfig | None, optional): :class:`SeasonConfig` override. Defaults to the library
|
|
166
167
|
agronomic defaults.
|
|
167
|
-
**entity_kwargs: Named filter pairs that identify the boundary,
|
|
168
|
+
**entity_kwargs (str | list[str]): Named filter pairs that identify the boundary,
|
|
168
169
|
e.g. ``tehsil="Pangi"``, ``district="Chamba"``,
|
|
169
170
|
``state="Himachal Pradesh"``, ``mws_id="13_551"``.
|
|
170
171
|
Mutually exclusive with ``bbox`` and ``geometry``.
|
|
@@ -177,6 +178,7 @@ class AoI:
|
|
|
177
178
|
"""
|
|
178
179
|
self.data_root = pathlib.Path(data_root).resolve()
|
|
179
180
|
self.seasons: SeasonConfig = seasons or SeasonConfig()
|
|
181
|
+
logger.info("Initializing AoI with data_root={}", self.data_root)
|
|
180
182
|
|
|
181
183
|
n_modes = sum(
|
|
182
184
|
[
|
|
@@ -186,11 +188,13 @@ class AoI:
|
|
|
186
188
|
]
|
|
187
189
|
)
|
|
188
190
|
if n_modes == 0:
|
|
191
|
+
logger.error("AoI initialization failed: no boundary argument supplied.")
|
|
189
192
|
raise ValueError(
|
|
190
193
|
"AoI requires exactly one boundary argument: "
|
|
191
194
|
"bbox=, geometry=, or entity keyword filters such as tehsil='Pangi'."
|
|
192
195
|
)
|
|
193
196
|
if n_modes > 1:
|
|
197
|
+
logger.error("AoI initialization failed: multiple boundary modes supplied.")
|
|
194
198
|
raise ValueError(
|
|
195
199
|
"bbox, geometry, and entity keyword filters are mutually exclusive. "
|
|
196
200
|
"Provide exactly one boundary mode."
|
|
@@ -219,7 +223,7 @@ class AoI:
|
|
|
219
223
|
"""The season name for today's date under the AoI's SeasonConfig.
|
|
220
224
|
|
|
221
225
|
Returns:
|
|
222
|
-
``"kharif"``, ``"rabi"``, or ``"zaid"``.
|
|
226
|
+
str: ``"kharif"``, ``"rabi"``, or ``"zaid"``.
|
|
223
227
|
"""
|
|
224
228
|
return self.seasons.season_for(date.today())
|
|
225
229
|
|
|
@@ -228,7 +232,7 @@ class AoI:
|
|
|
228
232
|
"""The current calendar year.
|
|
229
233
|
|
|
230
234
|
Returns:
|
|
231
|
-
Current year as an integer.
|
|
235
|
+
int: Current year as an integer.
|
|
232
236
|
"""
|
|
233
237
|
return date.today().year
|
|
234
238
|
|
|
@@ -236,11 +240,11 @@ class AoI:
|
|
|
236
240
|
"""Render an interactive Lonboard map of the AoI and its entity layers.
|
|
237
241
|
|
|
238
242
|
Args:
|
|
239
|
-
overlay: An optional :class:`~core_lens.base.result.Result` to
|
|
243
|
+
overlay (Result | None, optional): An optional :class:`~core_lens.base.result.Result` to
|
|
240
244
|
overlay on the map.
|
|
241
245
|
|
|
242
246
|
Returns:
|
|
243
|
-
A Lonboard Map object.
|
|
247
|
+
Any: A Lonboard Map object.
|
|
244
248
|
"""
|
|
245
249
|
import lonboard
|
|
246
250
|
import geopandas as gpd
|
|
@@ -309,10 +313,10 @@ class AoI:
|
|
|
309
313
|
the unique matching row).
|
|
310
314
|
|
|
311
315
|
Args:
|
|
312
|
-
entity_kwargs: Column–value pairs used to identify the boundary.
|
|
316
|
+
entity_kwargs (dict[str, str | list[str]]): Column–value pairs used to identify the boundary.
|
|
313
317
|
|
|
314
318
|
Returns:
|
|
315
|
-
The union of all matching entity geometries as a Shapely object.
|
|
319
|
+
shapely.Geometry: The union of all matching entity geometries as a Shapely object.
|
|
316
320
|
|
|
317
321
|
Raises:
|
|
318
322
|
:class:`~core_lens.base.EntityValidationError`: If no registered entity can satisfy the filters.
|
|
@@ -320,6 +324,8 @@ class AoI:
|
|
|
320
324
|
"""
|
|
321
325
|
import shapely.ops as sops
|
|
322
326
|
|
|
327
|
+
logger.debug("Resolving named boundary using kwargs: {}", entity_kwargs)
|
|
328
|
+
|
|
323
329
|
# Find the registered entity whose key_col or known attribute column
|
|
324
330
|
# matches one of the filter keys.
|
|
325
331
|
candidate: BaseEntity | None = None
|
|
@@ -335,12 +341,19 @@ class AoI:
|
|
|
335
341
|
# Fall back: look for an entity whose name matches a kwarg key
|
|
336
342
|
# (e.g. tehsil="Pangi" → TehsilEntity if registered as "tehsil").
|
|
337
343
|
if candidate is None:
|
|
344
|
+
logger.debug(
|
|
345
|
+
"No direct column match found for kwargs, attempting entity name fallback"
|
|
346
|
+
)
|
|
338
347
|
for name, entity in self._entity_instances.items():
|
|
339
348
|
if name in entity_kwargs:
|
|
340
349
|
candidate = entity
|
|
341
350
|
break
|
|
342
351
|
|
|
343
352
|
if candidate is None:
|
|
353
|
+
logger.error(
|
|
354
|
+
"Boundary resolution failed: no registered entity matched filters {}",
|
|
355
|
+
entity_kwargs,
|
|
356
|
+
)
|
|
344
357
|
raise EntityValidationError(
|
|
345
358
|
f"No registered entity can satisfy the filters {entity_kwargs}. "
|
|
346
359
|
f"Registered entities: {sorted(_REGISTRY)}."
|
|
@@ -367,6 +380,9 @@ class AoI:
|
|
|
367
380
|
df = lf.filter(filter_expr).select(candidate.key_cols + [geom_col]).collect()
|
|
368
381
|
|
|
369
382
|
if df.is_empty():
|
|
383
|
+
logger.error(
|
|
384
|
+
"Boundary resolution failed: no rows matched filters {}", entity_kwargs
|
|
385
|
+
)
|
|
370
386
|
raise ValueError(
|
|
371
387
|
f"No rows matched the filters {entity_kwargs} "
|
|
372
388
|
f"in {candidate.static_path!r}."
|
|
@@ -391,7 +407,7 @@ class AoI:
|
|
|
391
407
|
:class:`AoI` is instantiated (when ``data_root`` is known).
|
|
392
408
|
|
|
393
409
|
Args:
|
|
394
|
-
entity_cls: A concrete subclass of
|
|
410
|
+
entity_cls (type[BaseEntity]): A concrete subclass of
|
|
395
411
|
:class:`~core_lens.base.entity.BaseEntity`.
|
|
396
412
|
|
|
397
413
|
Raises:
|
|
@@ -401,6 +417,7 @@ class AoI:
|
|
|
401
417
|
import pathlib as _pathlib
|
|
402
418
|
|
|
403
419
|
name = _entity_name(entity_cls)
|
|
420
|
+
logger.info("Registering entity class: {}", name)
|
|
404
421
|
# Probe with a no-root instance to check if paths are absolute.
|
|
405
422
|
probe = entity_cls()
|
|
406
423
|
static = probe.static_path
|
|
@@ -417,9 +434,10 @@ class AoI:
|
|
|
417
434
|
Primarily useful in tests where a clean registry is needed between runs.
|
|
418
435
|
|
|
419
436
|
Args:
|
|
420
|
-
entity_cls: The entity class to remove.
|
|
437
|
+
entity_cls (type[BaseEntity]): The entity class to remove.
|
|
421
438
|
"""
|
|
422
439
|
name = _entity_name(entity_cls)
|
|
440
|
+
logger.info("Deregistering entity class: {}", name)
|
|
423
441
|
_REGISTRY.pop(name, None)
|
|
424
442
|
|
|
425
443
|
@classmethod
|
|
@@ -427,7 +445,7 @@ class AoI:
|
|
|
427
445
|
"""Return the names of all currently registered entities.
|
|
428
446
|
|
|
429
447
|
Returns:
|
|
430
|
-
A sorted list of entity name strings.
|
|
448
|
+
list[str]: A sorted list of entity name strings.
|
|
431
449
|
"""
|
|
432
450
|
return sorted(_REGISTRY)
|
|
433
451
|
|
|
@@ -452,11 +470,21 @@ def _validate_entity(entity: BaseEntity, name: str) -> None:
|
|
|
452
470
|
try:
|
|
453
471
|
static = entity._resolve(entity.static_path)
|
|
454
472
|
except FileNotFoundError:
|
|
473
|
+
logger.error(
|
|
474
|
+
"Validation failed for entity {}: static_path '{}' does not exist.",
|
|
475
|
+
name,
|
|
476
|
+
entity.static_path,
|
|
477
|
+
)
|
|
455
478
|
raise EntityValidationError(
|
|
456
479
|
f"Entity {name!r}: static_path {entity.static_path!r} does not exist."
|
|
457
480
|
)
|
|
458
481
|
|
|
459
482
|
if not os.path.exists(static):
|
|
483
|
+
logger.error(
|
|
484
|
+
"Validation failed for entity {}: static path '{}' does not exist.",
|
|
485
|
+
name,
|
|
486
|
+
static,
|
|
487
|
+
)
|
|
460
488
|
raise EntityValidationError(
|
|
461
489
|
f"Entity {name!r}: static_path {static!r} does not exist."
|
|
462
490
|
)
|
|
@@ -464,18 +492,34 @@ def _validate_entity(entity: BaseEntity, name: str) -> None:
|
|
|
464
492
|
try:
|
|
465
493
|
schema = pl.read_parquet_schema(static)
|
|
466
494
|
except Exception as exc:
|
|
495
|
+
logger.error(
|
|
496
|
+
"Validation failed for entity {}: could not read schema from '{}': {}",
|
|
497
|
+
name,
|
|
498
|
+
static,
|
|
499
|
+
exc,
|
|
500
|
+
)
|
|
467
501
|
raise EntityValidationError(
|
|
468
502
|
f"Entity {name!r}: could not read schema from {static!r}: {exc}"
|
|
469
503
|
) from exc
|
|
470
504
|
|
|
471
505
|
missing_keys = [c for c in entity.key_cols if c not in schema]
|
|
472
506
|
if missing_keys:
|
|
507
|
+
logger.error(
|
|
508
|
+
"Validation failed for entity {}: key columns {} not found in schema.",
|
|
509
|
+
name,
|
|
510
|
+
missing_keys,
|
|
511
|
+
)
|
|
473
512
|
raise EntityValidationError(
|
|
474
513
|
f"Entity {name!r}: key_cols {missing_keys} not found in {static!r}. "
|
|
475
514
|
f"Available columns: {list(schema.keys())}."
|
|
476
515
|
)
|
|
477
516
|
|
|
478
517
|
if entity.geometry_col not in schema:
|
|
518
|
+
logger.error(
|
|
519
|
+
"Validation failed for entity {}: geometry column '{}' not found in schema.",
|
|
520
|
+
name,
|
|
521
|
+
entity.geometry_col,
|
|
522
|
+
)
|
|
479
523
|
raise EntityValidationError(
|
|
480
524
|
f"Entity {name!r}: geometry_col {entity.geometry_col!r} "
|
|
481
525
|
f"not found in {static!r}. Available columns: {list(schema.keys())}."
|
|
@@ -489,6 +533,12 @@ def _validate_entity(entity: BaseEntity, name: str) -> None:
|
|
|
489
533
|
except FileNotFoundError:
|
|
490
534
|
abs_path = None
|
|
491
535
|
if abs_path is None or not os.path.exists(abs_path):
|
|
536
|
+
logger.error(
|
|
537
|
+
"Validation failed for entity {}: {} path '{}' does not exist.",
|
|
538
|
+
name,
|
|
539
|
+
label,
|
|
540
|
+
path,
|
|
541
|
+
)
|
|
492
542
|
raise EntityValidationError(
|
|
493
543
|
f"Entity {name!r}: {label}_path {path!r} does not exist."
|
|
494
544
|
)
|
|
@@ -16,6 +16,7 @@ from abc import ABC, abstractmethod
|
|
|
16
16
|
from typing import TYPE_CHECKING, Any
|
|
17
17
|
|
|
18
18
|
import polars as pl
|
|
19
|
+
from loguru import logger
|
|
19
20
|
|
|
20
21
|
from core_lens.utils.spatial import (
|
|
21
22
|
bbox_intersects_geometry,
|
|
@@ -84,7 +85,7 @@ class BaseEntity(ABC):
|
|
|
84
85
|
"""Initialise the entity with an optional data root directory.
|
|
85
86
|
|
|
86
87
|
Args:
|
|
87
|
-
data_root: Absolute path to the root data directory. When
|
|
88
|
+
data_root (pathlib.Path | None, optional): Absolute path to the root data directory. When
|
|
88
89
|
supplied, relative :attr:`static_path`, :attr:`annual_path`,
|
|
89
90
|
and :attr:`fortnightly_path` values are resolved against this
|
|
90
91
|
directory. Defaults to ``None``, in which case relative paths
|
|
@@ -100,10 +101,10 @@ class BaseEntity(ABC):
|
|
|
100
101
|
otherwise against the current working directory.
|
|
101
102
|
|
|
102
103
|
Args:
|
|
103
|
-
path: A filesystem path, absolute or relative.
|
|
104
|
+
path (str): A filesystem path, absolute or relative.
|
|
104
105
|
|
|
105
106
|
Returns:
|
|
106
|
-
An absolute path string.
|
|
107
|
+
str: An absolute path string.
|
|
107
108
|
|
|
108
109
|
Raises:
|
|
109
110
|
FileNotFoundError: If the resolved path does not exist.
|
|
@@ -115,6 +116,9 @@ class BaseEntity(ABC):
|
|
|
115
116
|
)
|
|
116
117
|
p = root / p
|
|
117
118
|
if not p.exists():
|
|
119
|
+
logger.error(
|
|
120
|
+
"Path resolution failed: '{}' (resolved to {}) does not exist.", path, p
|
|
121
|
+
)
|
|
118
122
|
raise FileNotFoundError(
|
|
119
123
|
f"Entity path {path!r} (resolved to {p}) does not exist. "
|
|
120
124
|
"Provide an absolute path or ensure the file exists relative to "
|
|
@@ -131,7 +135,7 @@ class BaseEntity(ABC):
|
|
|
131
135
|
``[\"mws_id\"]``), but the contract allows composite keys for plugins.
|
|
132
136
|
|
|
133
137
|
Returns:
|
|
134
|
-
A list of column name strings present in the static file.
|
|
138
|
+
list[str]: A list of column name strings present in the static file.
|
|
135
139
|
"""
|
|
136
140
|
|
|
137
141
|
@property
|
|
@@ -143,7 +147,7 @@ class BaseEntity(ABC):
|
|
|
143
147
|
(WKB bytes, WKT string, or a native geometry column).
|
|
144
148
|
|
|
145
149
|
Returns:
|
|
146
|
-
The column name as a string.
|
|
150
|
+
str: The column name as a string.
|
|
147
151
|
"""
|
|
148
152
|
|
|
149
153
|
@property
|
|
@@ -156,7 +160,7 @@ class BaseEntity(ABC):
|
|
|
156
160
|
``FileNotFoundError`` is raised if the file does not exist.
|
|
157
161
|
|
|
158
162
|
Returns:
|
|
159
|
-
A path string.
|
|
163
|
+
str: A path string.
|
|
160
164
|
"""
|
|
161
165
|
|
|
162
166
|
@property
|
|
@@ -168,7 +172,7 @@ class BaseEntity(ABC):
|
|
|
168
172
|
:class:`EntityValidationError` is raised.
|
|
169
173
|
|
|
170
174
|
Returns:
|
|
171
|
-
A path string, or ``None`` if the entity has no annual data.
|
|
175
|
+
str | None: A path string, or ``None`` if the entity has no annual data.
|
|
172
176
|
"""
|
|
173
177
|
return None
|
|
174
178
|
|
|
@@ -181,7 +185,7 @@ class BaseEntity(ABC):
|
|
|
181
185
|
:class:`EntityValidationError` is raised.
|
|
182
186
|
|
|
183
187
|
Returns:
|
|
184
|
-
A path string, or ``None`` if the entity has no fortnightly data.
|
|
188
|
+
str | None: A path string, or ``None`` if the entity has no fortnightly data.
|
|
185
189
|
"""
|
|
186
190
|
return None
|
|
187
191
|
|
|
@@ -194,7 +198,7 @@ class BaseEntity(ABC):
|
|
|
194
198
|
instead of relying on detection.
|
|
195
199
|
|
|
196
200
|
Returns:
|
|
197
|
-
A fully-validated :class:`~core_lens.schema.profile.SchemaProfile`.
|
|
201
|
+
SchemaProfile: A fully-validated :class:`~core_lens.schema.profile.SchemaProfile`.
|
|
198
202
|
"""
|
|
199
203
|
if not hasattr(self, "_schema_profile"):
|
|
200
204
|
from core_lens.schema.detection import detect
|
|
@@ -219,6 +223,9 @@ class BaseEntity(ABC):
|
|
|
219
223
|
@property
|
|
220
224
|
def _index(self) -> pl.DataFrame:
|
|
221
225
|
if not hasattr(self, "_cached_index"):
|
|
226
|
+
logger.debug(
|
|
227
|
+
"Building lazy bounding box index for {}", self.__class__.__name__
|
|
228
|
+
)
|
|
222
229
|
profile = self.schema_profile
|
|
223
230
|
self._cached_index: pl.DataFrame = build_bbox_index(
|
|
224
231
|
static_path=self._resolve(self.static_path),
|
|
@@ -247,7 +254,7 @@ class BaseEntity(ABC):
|
|
|
247
254
|
are resolved as entity-name lookups.
|
|
248
255
|
|
|
249
256
|
Returns:
|
|
250
|
-
A lazy :class:`~core_lens.base.view.View` with resolved key pairs.
|
|
257
|
+
View: A lazy :class:`~core_lens.base.view.View` with resolved key pairs.
|
|
251
258
|
|
|
252
259
|
Raises:
|
|
253
260
|
ValueError: If a kwarg cannot be resolved as either an attribute
|
|
@@ -256,6 +263,10 @@ class BaseEntity(ABC):
|
|
|
256
263
|
from core_lens.aoi import _REGISTRY
|
|
257
264
|
from core_lens.base.view import View
|
|
258
265
|
|
|
266
|
+
logger.debug(
|
|
267
|
+
"Applying attribute filters on {}: {}", self.__class__.__name__, kwargs
|
|
268
|
+
)
|
|
269
|
+
|
|
259
270
|
static = self._resolve(self.static_path)
|
|
260
271
|
schema = pl.read_parquet_schema(static)
|
|
261
272
|
|
|
@@ -265,6 +276,10 @@ class BaseEntity(ABC):
|
|
|
265
276
|
# Validate entity-kwargs early so we give a useful error message.
|
|
266
277
|
for k in entity_kwargs:
|
|
267
278
|
if k not in _REGISTRY:
|
|
279
|
+
logger.error(
|
|
280
|
+
"BaseEntity.where failed: '{}' is neither a column nor a registered entity.",
|
|
281
|
+
k,
|
|
282
|
+
)
|
|
268
283
|
raise ValueError(
|
|
269
284
|
f"BaseEntity.where: {k!r} is not a column in {self.static_path!r} "
|
|
270
285
|
f"and is not a registered entity name. "
|
|
@@ -306,6 +321,12 @@ class BaseEntity(ABC):
|
|
|
306
321
|
)
|
|
307
322
|
|
|
308
323
|
if matched.is_empty():
|
|
324
|
+
logger.error(
|
|
325
|
+
"BaseEntity.where failed: No rows matched {}={!r} in {}",
|
|
326
|
+
entity_kwarg_name,
|
|
327
|
+
entity_kwarg_val,
|
|
328
|
+
other_entity.static_path,
|
|
329
|
+
)
|
|
309
330
|
raise ValueError(
|
|
310
331
|
f"BaseEntity.where: No rows matched {entity_kwarg_name}={entity_kwarg_val!r} "
|
|
311
332
|
f"in {other_entity.static_path!r}."
|
|
@@ -354,21 +375,21 @@ class BaseEntity(ABC):
|
|
|
354
375
|
refines with a Shapely STRtree exact-relationship check.
|
|
355
376
|
|
|
356
377
|
Args:
|
|
357
|
-
geometry: A Shapely geometry representing the spatial extent.
|
|
358
|
-
bbox: Bounding box as ``(minx, miny, maxx, maxy)`` in WGS-84.
|
|
378
|
+
geometry (shapely.Geometry | None, optional): A Shapely geometry representing the spatial extent.
|
|
379
|
+
bbox (tuple[float, float, float, float] | None, optional): Bounding box as ``(minx, miny, maxx, maxy)`` in WGS-84.
|
|
359
380
|
Converted to a ``shapely.geometry.box`` internally.
|
|
360
|
-
relationship: Spatial relationship mode.
|
|
381
|
+
relationship (str, optional): Spatial relationship mode.
|
|
361
382
|
|
|
362
383
|
* ``"centroid"`` (default) — entity centroid must lie within
|
|
363
384
|
the geometry.
|
|
364
385
|
* ``"area"`` — intersection area / entity area must exceed
|
|
365
386
|
``threshold``.
|
|
366
387
|
|
|
367
|
-
threshold: Area coverage threshold for ``"area"`` mode (0–1).
|
|
388
|
+
threshold (float, optional): Area coverage threshold for ``"area"`` mode (0–1).
|
|
368
389
|
Default 0.5.
|
|
369
390
|
|
|
370
391
|
Returns:
|
|
371
|
-
A lazy :class:`~core_lens.base.view.View` scoped to the given
|
|
392
|
+
View: A lazy :class:`~core_lens.base.view.View` scoped to the given
|
|
372
393
|
spatial extent.
|
|
373
394
|
|
|
374
395
|
Raises:
|
|
@@ -379,6 +400,9 @@ class BaseEntity(ABC):
|
|
|
379
400
|
from core_lens.base.view import View
|
|
380
401
|
|
|
381
402
|
if geometry is None and bbox is None:
|
|
403
|
+
logger.error(
|
|
404
|
+
"BaseEntity.spatial_filter failed: neither 'geometry' nor 'bbox' provided."
|
|
405
|
+
)
|
|
382
406
|
raise ValueError(
|
|
383
407
|
"spatial_filter() requires either 'geometry' or 'bbox' to be provided."
|
|
384
408
|
)
|
|
@@ -387,6 +411,12 @@ class BaseEntity(ABC):
|
|
|
387
411
|
|
|
388
412
|
assert geometry is not None # guaranteed by the guards above
|
|
389
413
|
|
|
414
|
+
logger.debug(
|
|
415
|
+
"Applying spatial filter on {} (relationship='{}')",
|
|
416
|
+
self.__class__.__name__,
|
|
417
|
+
relationship,
|
|
418
|
+
)
|
|
419
|
+
|
|
390
420
|
profile = self.schema_profile
|
|
391
421
|
candidates = bbox_intersects_geometry(self._index, geometry)
|
|
392
422
|
keys = exact_spatial_filter(
|
|
@@ -411,19 +441,25 @@ class BaseEntity(ABC):
|
|
|
411
441
|
Joined columns are namespaced as ``{entity_name}_{column_name}``.
|
|
412
442
|
|
|
413
443
|
Args:
|
|
414
|
-
other: The secondary :class:`BaseEntity` whose columns will be
|
|
444
|
+
other (BaseEntity): The secondary :class:`BaseEntity` whose columns will be
|
|
415
445
|
joined and aggregated onto ``self``.
|
|
416
|
-
agg: Mapping of ``{column: aggregation}`` specifying which columns
|
|
446
|
+
agg (dict[str, str]): Mapping of ``{column: aggregation}`` specifying which columns
|
|
417
447
|
from ``other`` to bring in and how to aggregate them. Valid
|
|
418
448
|
aggregation strings are ``\"area\"``, ``\"count\"``, ``\"mean\"``,
|
|
419
449
|
``\"sum\"``, ``\"min\"``, and ``\"max\"``.
|
|
420
450
|
|
|
421
451
|
Returns:
|
|
422
|
-
A lazy :class:`~core_lens.base.view.View` with the join spec
|
|
452
|
+
View: A lazy :class:`~core_lens.base.view.View` with the join spec
|
|
423
453
|
recorded for deferred execution.
|
|
424
454
|
"""
|
|
425
455
|
from core_lens.base.view import View
|
|
426
456
|
|
|
457
|
+
logger.debug(
|
|
458
|
+
"Deferred spatial join registered for {} with {}",
|
|
459
|
+
self.__class__.__name__,
|
|
460
|
+
other.__class__.__name__,
|
|
461
|
+
)
|
|
462
|
+
|
|
427
463
|
entity_name = _entity_name(type(self))
|
|
428
464
|
join_spec = {"other": other, "agg": agg}
|
|
429
465
|
return View(
|