core-lens 0.1.dev117__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.dev117 → core_lens-0.1.dev120}/PKG-INFO +8 -1
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/index.rst +1 -0
- core_lens-0.1.dev120/docs/source/logging.md +32 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/pyproject.toml +6 -1
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/__init__.py +4 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/aoi.py +50 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/base/entity.py +36 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/base/view.py +34 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/utils/spatial.py +30 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_entity.py +11 -3
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/uv.lock +251 -251
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/.gitignore +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/.gitmessage +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/.python-version +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/LICENSE +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/README.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/SKILLS.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/Makefile +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/make.bat +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/export.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/queries.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/docs/source/stats.md +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/base/namespaces/plot.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/base/namespaces/stats.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/base/result.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/entities/mws.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/entities/tehsil.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/export/formats.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/schema/detection.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/schema/profile.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/utils/polars_utils.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/src/core_lens/utils/season.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_plot.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev117 → core_lens-0.1.dev120}/tests/unit/test_view.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,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.
|
|
@@ -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
|
|
|
@@ -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."
|
|
@@ -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}."
|
|
@@ -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
|
|
@@ -420,6 +437,7 @@ class AoI:
|
|
|
420
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
|
|
@@ -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,
|
|
@@ -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 "
|
|
@@ -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),
|
|
@@ -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}."
|
|
@@ -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(
|
|
@@ -424,6 +454,12 @@ class BaseEntity(ABC):
|
|
|
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(
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
import polars as pl
|
|
8
|
+
from loguru import logger
|
|
8
9
|
|
|
9
10
|
from enum import Enum
|
|
10
11
|
|
|
@@ -145,6 +146,9 @@ class View:
|
|
|
145
146
|
)
|
|
146
147
|
|
|
147
148
|
if geometry is None and bbox is None:
|
|
149
|
+
logger.error(
|
|
150
|
+
"View.spatial_filter failed: neither 'geometry' nor 'bbox' provided."
|
|
151
|
+
)
|
|
148
152
|
raise ValueError(
|
|
149
153
|
"spatial_filter() requires either 'geometry' or 'bbox' to be provided."
|
|
150
154
|
)
|
|
@@ -193,6 +197,9 @@ class View:
|
|
|
193
197
|
View: A new lazy :class:`View` with the join spec recorded.
|
|
194
198
|
"""
|
|
195
199
|
if self.join_spec is not None:
|
|
200
|
+
logger.error(
|
|
201
|
+
"View.spatial_join failed: View already has a pending spatial_join."
|
|
202
|
+
)
|
|
196
203
|
raise ValueError("View already has a pending spatial_join.")
|
|
197
204
|
|
|
198
205
|
return View(
|
|
@@ -252,23 +259,33 @@ class View:
|
|
|
252
259
|
supplying ``year`` with ``season="current"``).
|
|
253
260
|
"""
|
|
254
261
|
if season is not None and (start is not None or end is not None):
|
|
262
|
+
logger.error(
|
|
263
|
+
"View.between failed: Date range (start/end) and season mode are mutually exclusive."
|
|
264
|
+
)
|
|
255
265
|
raise ValueError(
|
|
256
266
|
"View.between: Date range (start/end) and season mode are mutually exclusive. "
|
|
257
267
|
"Use either positional date strings or the 'season' keyword, not both."
|
|
258
268
|
)
|
|
259
269
|
|
|
260
270
|
if season is None and year is not None:
|
|
271
|
+
logger.error(
|
|
272
|
+
"View.between failed: 'year' argument provided without 'season'."
|
|
273
|
+
)
|
|
261
274
|
raise ValueError(
|
|
262
275
|
"View.between: 'year' argument is only valid when 'season' is also provided."
|
|
263
276
|
)
|
|
264
277
|
|
|
265
278
|
if season is not None:
|
|
266
279
|
if not isinstance(season, Season):
|
|
280
|
+
logger.error("View.between failed: season is not a Season enum.")
|
|
267
281
|
raise ValueError(
|
|
268
282
|
f"View.between: season must be a Season enum. "
|
|
269
283
|
f"Valid options: {[e.name for e in Season]}."
|
|
270
284
|
)
|
|
271
285
|
if season is Season.CURRENT and year is not None:
|
|
286
|
+
logger.error(
|
|
287
|
+
"View.between failed: Cannot combine year with season='current'."
|
|
288
|
+
)
|
|
272
289
|
raise ValueError(
|
|
273
290
|
"View.between: Cannot combine year with season='current'. "
|
|
274
291
|
"The current season is always resolved to the present calendar date."
|
|
@@ -278,6 +295,9 @@ class View:
|
|
|
278
295
|
time_filter["year"] = year
|
|
279
296
|
else:
|
|
280
297
|
if start is None or end is None:
|
|
298
|
+
logger.error(
|
|
299
|
+
"View.between failed: Both 'start' and 'end' must be provided for date range mode."
|
|
300
|
+
)
|
|
281
301
|
raise ValueError(
|
|
282
302
|
"View.between: Both 'start' and 'end' must be provided for date range mode."
|
|
283
303
|
)
|
|
@@ -340,12 +360,20 @@ class View:
|
|
|
340
360
|
|
|
341
361
|
profile = self.entity.schema_profile
|
|
342
362
|
|
|
363
|
+
logger.info(
|
|
364
|
+
"Materialising {} resolution for {}", resolution.name, self.entity_name
|
|
365
|
+
)
|
|
366
|
+
|
|
343
367
|
path: str
|
|
344
368
|
if resolution == Resolution.STATIC:
|
|
345
369
|
path = self.entity.static_path
|
|
346
370
|
elif resolution == Resolution.ANNUAL:
|
|
347
371
|
annual_path = self.entity.annual_path
|
|
348
372
|
if annual_path is None:
|
|
373
|
+
logger.error(
|
|
374
|
+
"View._materialise failed: Entity '{}' has no annual_path declared.",
|
|
375
|
+
self.entity_name,
|
|
376
|
+
)
|
|
349
377
|
raise AttributeError(
|
|
350
378
|
f"Entity {self.entity_name!r} has no annual_path declared."
|
|
351
379
|
)
|
|
@@ -353,6 +381,10 @@ class View:
|
|
|
353
381
|
else:
|
|
354
382
|
fn_path = self.entity.fortnightly_path
|
|
355
383
|
if fn_path is None:
|
|
384
|
+
logger.error(
|
|
385
|
+
"View._materialise failed: Entity '{}' has no fortnightly_path declared.",
|
|
386
|
+
self.entity_name,
|
|
387
|
+
)
|
|
356
388
|
raise AttributeError(
|
|
357
389
|
f"Entity {self.entity_name!r} has no fortnightly_path declared."
|
|
358
390
|
)
|
|
@@ -396,6 +428,7 @@ class View:
|
|
|
396
428
|
_tf = dict(self.time_filter)
|
|
397
429
|
if "start" in _tf and "end" in _tf:
|
|
398
430
|
_tf["_is_year_col"] = _is_year_col
|
|
431
|
+
logger.debug("Applying time filter expr using {}", _tf)
|
|
399
432
|
time_expr = resolve_time_filter(_tf, time_col, season_cfg)
|
|
400
433
|
|
|
401
434
|
lf = scan_with_key_filter(
|
|
@@ -439,6 +472,7 @@ class View:
|
|
|
439
472
|
|
|
440
473
|
from core_lens.utils.spatial import execute_spatial_join
|
|
441
474
|
|
|
475
|
+
logger.debug("Executing spatial join with {}", other_entity_name)
|
|
442
476
|
data = execute_spatial_join(
|
|
443
477
|
primary_df=data,
|
|
444
478
|
primary_key_cols=self.entity.key_cols,
|
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
7
7
|
|
|
8
8
|
import polars as pl
|
|
9
9
|
import shapely
|
|
10
|
+
from loguru import logger
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
pass
|
|
@@ -28,6 +29,9 @@ def resolve_path(path: str) -> str:
|
|
|
28
29
|
if not p.is_absolute():
|
|
29
30
|
p = pathlib.Path.cwd() / p
|
|
30
31
|
if not p.exists():
|
|
32
|
+
logger.error(
|
|
33
|
+
"Path resolution failed: '{}' (resolved to {}) does not exist.", path, p
|
|
34
|
+
)
|
|
31
35
|
raise FileNotFoundError(
|
|
32
36
|
f"Entity path {path!r} (resolved to {p}) does not exist. "
|
|
33
37
|
"Provide an absolute path or ensure the file exists relative to "
|
|
@@ -62,6 +66,7 @@ def build_bbox_index(
|
|
|
62
66
|
pl.DataFrame: A ``pl.DataFrame`` with columns ``(*key_cols, minx, miny, maxx, maxy)``.
|
|
63
67
|
"""
|
|
64
68
|
if bbox_cols is not None:
|
|
69
|
+
logger.debug("Using pre-computed bbox columns: {}", bbox_cols)
|
|
65
70
|
cols_to_read = key_cols + list(bbox_cols)
|
|
66
71
|
df = pl.read_parquet(static_path, columns=cols_to_read)
|
|
67
72
|
minx_col, miny_col, maxx_col, maxy_col = bbox_cols
|
|
@@ -72,6 +77,9 @@ def build_bbox_index(
|
|
|
72
77
|
cols_to_read = key_cols + [geometry_col]
|
|
73
78
|
if geometry_type == "latlon":
|
|
74
79
|
# For latlon, bbox_cols must be set — this path should not be reached.
|
|
80
|
+
logger.error(
|
|
81
|
+
"build_bbox_index failed: geometry_type='latlon' requires bbox_cols to be declared."
|
|
82
|
+
)
|
|
75
83
|
raise ValueError(
|
|
76
84
|
"geometry_type='latlon' requires bbox_cols to be declared on the entity. "
|
|
77
85
|
+ "Cannot compute bounds from separate lat/lon columns without bbox hints."
|
|
@@ -79,6 +87,11 @@ def build_bbox_index(
|
|
|
79
87
|
|
|
80
88
|
import pyarrow.dataset as ds # type: ignore[import-untyped]
|
|
81
89
|
|
|
90
|
+
logger.debug(
|
|
91
|
+
"No pre-computed bbox found in {}; falling back to Shapely decoding",
|
|
92
|
+
static_path,
|
|
93
|
+
)
|
|
94
|
+
|
|
82
95
|
dataset = ds.dataset(static_path)
|
|
83
96
|
chunks = []
|
|
84
97
|
|
|
@@ -184,7 +197,16 @@ def exact_spatial_filter(
|
|
|
184
197
|
"""
|
|
185
198
|
import shapely
|
|
186
199
|
|
|
200
|
+
logger.debug(
|
|
201
|
+
"Refining {} candidates with exact spatial filter (relationship='{}')",
|
|
202
|
+
len(candidates),
|
|
203
|
+
relationship,
|
|
204
|
+
)
|
|
205
|
+
|
|
187
206
|
if relationship not in ("centroid", "area"):
|
|
207
|
+
logger.error(
|
|
208
|
+
"exact_spatial_filter failed: Unknown relationship='{}'.", relationship
|
|
209
|
+
)
|
|
188
210
|
raise ValueError(
|
|
189
211
|
f"exact_spatial_filter: Unknown relationship={relationship!r}. "
|
|
190
212
|
"Valid options: 'centroid', 'area'."
|
|
@@ -271,6 +293,10 @@ def execute_spatial_join(
|
|
|
271
293
|
"""
|
|
272
294
|
import shapely
|
|
273
295
|
|
|
296
|
+
logger.info(
|
|
297
|
+
"Starting execute_spatial_join for other_entity_name={}", other_entity_name
|
|
298
|
+
)
|
|
299
|
+
|
|
274
300
|
other_profile = other_entity.schema_profile
|
|
275
301
|
other_static = other_entity._resolve(other_entity.static_path)
|
|
276
302
|
other_geom_col = other_profile.geometry_col
|
|
@@ -304,6 +330,10 @@ def execute_spatial_join(
|
|
|
304
330
|
|
|
305
331
|
# Decode primary geometries.
|
|
306
332
|
if primary_geom_col not in primary_df.columns:
|
|
333
|
+
logger.error(
|
|
334
|
+
"execute_spatial_join failed: geometry column '{}' not found in primary DataFrame.",
|
|
335
|
+
primary_geom_col,
|
|
336
|
+
)
|
|
307
337
|
raise ValueError(
|
|
308
338
|
f"execute_spatial_join: geometry column {primary_geom_col!r} not found "
|
|
309
339
|
"in primary DataFrame. Materialise static resolution or call "
|
|
@@ -48,9 +48,17 @@ class TestEntityWhere:
|
|
|
48
48
|
).write_parquet(district_path)
|
|
49
49
|
|
|
50
50
|
class CustomDistrictEntity(BaseEntity):
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
@property
|
|
52
|
+
def key_cols(self) -> list[str]:
|
|
53
|
+
return ["district_id"]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def geometry_col(self) -> str:
|
|
57
|
+
return "geometry"
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def static_path(self) -> str:
|
|
61
|
+
return str(district_path)
|
|
54
62
|
|
|
55
63
|
@property
|
|
56
64
|
def schema_profile(self) -> Any:
|