core-lens 0.1.dev136__tar.gz → 0.1.dev140__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 (91) hide show
  1. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/PKG-INFO +1 -1
  2. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/bench_aoi.py +1 -2
  3. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/bench_export.py +1 -2
  4. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/bench_polars_utils.py +1 -2
  5. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/bench_result.py +1 -2
  6. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/bench_schema.py +1 -2
  7. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/bench_season.py +1 -2
  8. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/bench_spatial.py +1 -2
  9. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/bench_view.py +1 -2
  10. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/export.md +8 -2
  11. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/intro.md +3 -1
  12. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/queries.md +3 -2
  13. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/quickstart.md +4 -4
  14. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/examples/demo_mws.py +1 -2
  15. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/examples/demo_tehsil.py +1 -2
  16. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/_version.py +2 -2
  17. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/aoi.py +17 -0
  18. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/base/entity.py +18 -3
  19. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/base/namespaces/plot.py +12 -5
  20. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/base/namespaces/stats.py +15 -1
  21. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/base/result.py +9 -0
  22. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/base/view.py +8 -0
  23. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/entities/mws.py +25 -10
  24. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/entities/tehsil.py +20 -8
  25. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/entities/waterbody.py +20 -8
  26. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/export/formats.py +5 -0
  27. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/schema/detection.py +10 -0
  28. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/schema/profile.py +2 -0
  29. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/utils/paths.py +4 -0
  30. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/utils/polars_utils.py +3 -0
  31. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/utils/season.py +7 -0
  32. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/utils/spatial.py +25 -4
  33. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/conftest.py +4 -1
  34. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_aoi.py +3 -0
  35. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_export.py +2 -1
  36. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_result.py +1 -0
  37. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_schema_detection.py +1 -0
  38. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_schema_profile.py +1 -0
  39. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_view.py +3 -1
  40. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  41. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  42. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  43. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.github/pull_request_template.md +0 -0
  44. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.github/workflows/ci.yml +0 -0
  45. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.github/workflows/gh-pages.yml +0 -0
  46. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.github/workflows/pre-release.yml +0 -0
  47. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.github/workflows/release.yml +0 -0
  48. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.gitignore +0 -0
  49. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.gitmessage +0 -0
  50. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.pre-commit-config.yaml +0 -0
  51. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/.python-version +0 -0
  52. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/CONTRIBUTING.md +0 -0
  53. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/LICENSE +0 -0
  54. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/README.md +0 -0
  55. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/SKILLS.md +0 -0
  56. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/README.md +0 -0
  57. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/benchmarks/run_all.sh +0 -0
  58. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/Makefile +0 -0
  59. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/make.bat +0 -0
  60. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/concepts.md +0 -0
  61. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/conf.py +0 -0
  62. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/index.rst +0 -0
  63. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/logging.md +0 -0
  64. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/plots.md +0 -0
  65. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/plugins.md +0 -0
  66. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/docs/source/stats.md +0 -0
  67. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/hooks/mypy.sh +0 -0
  68. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/hooks/no-parquet-outside-fixtures.sh +0 -0
  69. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/hooks/pytest.sh +0 -0
  70. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/pyproject.toml +0 -0
  71. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/__init__.py +0 -0
  72. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/__main__.py +0 -0
  73. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/base/__init__.py +0 -0
  74. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/base/namespaces/__init__.py +0 -0
  75. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/entities/__init__.py +0 -0
  76. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/export/__init__.py +0 -0
  77. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/py.typed +0 -0
  78. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/schema/__init__.py +0 -0
  79. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/src/core_lens/utils/__init__.py +0 -0
  80. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/fixtures/generate_fixtures.py +0 -0
  81. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_entities.py +0 -0
  82. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_entity.py +0 -0
  83. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_main.py +0 -0
  84. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_plot.py +0 -0
  85. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_polars_utils.py +0 -0
  86. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_profile.py +0 -0
  87. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_season.py +0 -0
  88. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_season_config.py +0 -0
  89. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_spatial.py +0 -0
  90. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/tests/unit/test_stats.py +0 -0
  91. {core_lens-0.1.dev136 → core_lens-0.1.dev140}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-lens
3
- Version: 0.1.dev136
3
+ Version: 0.1.dev140
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
@@ -1,5 +1,4 @@
1
- """
2
- bench_aoi.py — Benchmark for core_lens.aoi
1
+ """bench_aoi.py — Benchmark for core_lens.aoi
3
2
  ===========================================
4
3
  Targets:
5
4
  - AoI.register() entity registration + path probe
@@ -1,5 +1,4 @@
1
- """
2
- bench_export.py — Benchmark for core_lens.export.formats
1
+ """bench_export.py — Benchmark for core_lens.export.formats
3
2
  =========================================================
4
3
  Targets:
5
4
  - export.parquet() Polars write_parquet (no geometry)
@@ -1,5 +1,4 @@
1
- """
2
- bench_polars_utils.py — Benchmark for core_lens.utils.polars_utils
1
+ """bench_polars_utils.py — Benchmark for core_lens.utils.polars_utils
3
2
  ===================================================================
4
3
  Targets:
5
4
  - _gpu_available() module-level probe + caching
@@ -1,5 +1,4 @@
1
- """
2
- bench_result.py — Benchmark for core_lens.base.result.Result
1
+ """bench_result.py — Benchmark for core_lens.base.result.Result
3
2
  =============================================================
4
3
  Targets:
5
4
  - Result.df() zero-copy accessor
@@ -1,5 +1,4 @@
1
- """
2
- bench_schema.py — Benchmark for core_lens.schema.detection
1
+ """bench_schema.py — Benchmark for core_lens.schema.detection
3
2
  ============================================================
4
3
  Targets:
5
4
  - detect() full schema auto-detection from Parquet metadata
@@ -1,5 +1,4 @@
1
- """
2
- bench_season.py — Benchmark for core_lens.utils.season
1
+ """bench_season.py — Benchmark for core_lens.utils.season
3
2
  =======================================================
4
3
  Targets:
5
4
  - SeasonConfig.__post_init__ validation cost (datetime.strptime × 6)
@@ -1,5 +1,4 @@
1
- """
2
- bench_spatial.py — Benchmark for core_lens.utils.spatial
1
+ """bench_spatial.py — Benchmark for core_lens.utils.spatial
3
2
  =========================================================
4
3
  Targets:
5
4
  - build_bbox_index() cold build (Shapely decode path) vs pre-computed bbox path
@@ -1,5 +1,4 @@
1
- """
2
- bench_view.py — Benchmark for core_lens.base.view.View
1
+ """bench_view.py — Benchmark for core_lens.base.view.View
3
2
  =======================================================
4
3
  Targets:
5
4
  - View.where() attribute filter (Polars semi-join on static file)
@@ -41,7 +41,13 @@ geoparquet(
41
41
 
42
42
  # Export to GeoJSON
43
43
  geojson(spatial_res, "output.json")
44
+
45
+ # Export to newline-delimited GeoJSON (GeoJSONSeq)
46
+ geojson(spatial_res, "output.ndjson", driver="GeoJSONSeq")
44
47
  ```
45
48
 
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.
49
+ ```{note}
50
+ Exporting to **GeoParquet** 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.
51
+
52
+ Exporting to **GeoJSON** is fully natively streamed directly from Polars and Shapely without invoking GDAL or DuckDB, making it extremely fast for large datasets. Driver-specific kwargs for GeoJSON are ignored.
53
+ ```
@@ -13,4 +13,6 @@ CoreLens provides a unified interface over microwatersheds, administrative bound
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
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.
16
+ - **Cloud & Local Data**: Native support for reading Parquet data from local filesystems or directly from cloud storage (e.g., `s3://`).
17
+ - **High-Performance Exports**: Export results directly to Parquet, JSON, CSV, or spatially-enabled GeoParquet and GeoJSON (with a fast streaming writer).
18
+ - **Advanced Caching**: Transparently caches boundaries and pre-computes spatial indices (via on-disk sidecars) to achieve sub-second query initialisation times.
@@ -58,5 +58,6 @@ joined_view = aoi.mws.spatial_join(
58
58
  # res = joined_view.annual
59
59
  ```
60
60
 
61
- > [!NOTE]
62
- > Cross-entity spatial join execution (materialisation) is currently under development and will be added in a subsequent release. Calling materialisation methods (like `.annual`, `.static`, or `.fortnightly`) on a joined view will raise a `NotImplementedError` in the current version.
61
+ ```{note}
62
+ Cross-entity spatial join execution (materialisation) is currently under development and will be added in a subsequent release. Calling materialisation methods (like `.annual`, `.static`, or `.fortnightly`) on a joined view will raise a `NotImplementedError` in the current version.
63
+ ```
@@ -25,18 +25,18 @@ AoI.register(MWSEntity)
25
25
  AoI.register(TehsilEntity)
26
26
  ```
27
27
 
28
- Create an `AoI` (Area of Interest) by specifying the `data_root` and your boundary (either by name, bounding box, or Shapely geometry):
28
+ Create an `AoI` (Area of Interest) by specifying the `data_root` (which can be a local path or a cloud URI like `s3://bucket/data`) and your boundary (either by name, bounding box, or Shapely geometry):
29
29
 
30
30
  ```python
31
- # Create an AoI scoped to a specific Tehsil
31
+ # Create an AoI scoped to a specific Tehsil using a local path
32
32
  aoi = AoI(
33
33
  data_root="/path/to/data",
34
34
  tehsil="Pangi"
35
35
  )
36
36
 
37
- # Or scope it directly to a specific list of entity IDs
37
+ # Or scope it directly to a specific list of entity IDs using an S3 bucket
38
38
  aoi_specific = AoI(
39
- data_root="/path/to/data",
39
+ data_root="s3://my-cloud-bucket/core-lens-data",
40
40
  mws_id=["13_001", "13_002"]
41
41
  )
42
42
 
@@ -1,5 +1,4 @@
1
- """
2
- demo_mws.py — core_lens MWSEntity capability demonstration
1
+ """demo_mws.py — core_lens MWSEntity capability demonstration
3
2
  =========================================================
4
3
  Walks through every meaningful operation available on a spatiotemporal
5
4
  entity: registration, AoI construction, filtering, materialisation,
@@ -1,5 +1,4 @@
1
- """
2
- demo_tehsil.py — core_lens TehsilEntity capability demonstration
1
+ """demo_tehsil.py — core_lens TehsilEntity capability demonstration
3
2
  =================================================================
4
3
  Walks through every meaningful operation available on a static-only
5
4
  entity: registration, AoI construction, filtering, materialisation,
@@ -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.dev136'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev136')
21
+ __version__ = version = '0.1.dev140'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev140')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -47,6 +47,7 @@ class SeasonConfig:
47
47
  kharif: Kharif (monsoon) season range as ``(start_MM-DD, end_MM-DD)``.
48
48
  rabi: Rabi (winter) season range.
49
49
  zaid: Zaid (summer) season range.
50
+
50
51
  """
51
52
 
52
53
  kharif: tuple[str, str] = ("07-01", "10-31")
@@ -76,6 +77,7 @@ class SeasonConfig:
76
77
 
77
78
  Returns:
78
79
  str: ``"kharif"``, ``"rabi"``, or ``"zaid"``.
80
+
79
81
  """
80
82
  md = f"{d.month:02d}-{d.day:02d}"
81
83
  for name in ("kharif", "rabi", "zaid"):
@@ -115,6 +117,7 @@ def _default_season_config() -> SeasonConfig:
115
117
 
116
118
  Returns:
117
119
  SeasonConfig: The shared default instance.
120
+
118
121
  """
119
122
  global _DEFAULT_SEASON_CONFIG
120
123
  if _DEFAULT_SEASON_CONFIG is None:
@@ -167,6 +170,7 @@ def _cached_resolve_boundary(
167
170
 
168
171
  Returns:
169
172
  tuple: ``(geometry_wkb, entity_name, key_rows)``.
173
+
170
174
  """
171
175
  from core_lens.base.entity import _entity_name as _ename
172
176
 
@@ -284,6 +288,7 @@ class AoI:
284
288
  data_root: Resolved path to the data directory.
285
289
  geometry: Shapely polygon representing the AoI boundary.
286
290
  seasons: :class:`SeasonConfig` in effect for this AoI.
291
+
287
292
  """
288
293
 
289
294
  def __init__(
@@ -331,6 +336,7 @@ class AoI:
331
336
  one boundary mode is used simultaneously.
332
337
  :class:`~core_lens.base.EntityValidationError`: If a boundary entity referenced in
333
338
  ``entity_kwargs`` is not registered.
339
+
334
340
  """
335
341
  # Preserve cloud URIs as-is; resolve local paths to absolute.
336
342
  data_root_str = str(data_root)
@@ -395,6 +401,7 @@ class AoI:
395
401
 
396
402
  Returns:
397
403
  str: ``"kharif"``, ``"rabi"``, or ``"zaid"``.
404
+
398
405
  """
399
406
  return self.seasons.season_for(date.today())
400
407
 
@@ -404,6 +411,7 @@ class AoI:
404
411
 
405
412
  Returns:
406
413
  int: Current year as an integer.
414
+
407
415
  """
408
416
  return date.today().year
409
417
 
@@ -416,6 +424,7 @@ class AoI:
416
424
 
417
425
  Returns:
418
426
  Any: A Lonboard Map object.
427
+
419
428
  """
420
429
  import lonboard
421
430
  import geopandas as gpd
@@ -476,6 +485,7 @@ class AoI:
476
485
  Raises:
477
486
  KeyError: If *name* is not in :data:`_REGISTRY`.
478
487
  :class:`~core_lens.base.EntityValidationError`: If validation fails.
488
+
479
489
  """
480
490
  if name not in self._entity_instances:
481
491
  entity_cls = _REGISTRY[name]
@@ -506,6 +516,7 @@ class AoI:
506
516
  Raises:
507
517
  :class:`~core_lens.base.EntityValidationError`: If any registered
508
518
  entity fails validation.
519
+
509
520
  """
510
521
  for name in _REGISTRY:
511
522
  self._get_entity(name)
@@ -562,6 +573,7 @@ class AoI:
562
573
  Raises:
563
574
  :class:`~core_lens.base.EntityValidationError`: If no registered entity can satisfy the filters.
564
575
  ValueError: If the filters match zero rows.
576
+
565
577
  """
566
578
  logger.debug("Resolving named boundary using kwargs: {}", entity_kwargs)
567
579
 
@@ -625,6 +637,7 @@ class AoI:
625
637
  Raises:
626
638
  :class:`~core_lens.base.EntityValidationError`: If any validation check
627
639
  fails (absolute-path entities only at register time).
640
+
628
641
  """
629
642
  import pathlib as _pathlib
630
643
 
@@ -647,6 +660,7 @@ class AoI:
647
660
 
648
661
  Args:
649
662
  entity_cls (type[BaseEntity]): The entity class to remove.
663
+
650
664
  """
651
665
  name = _entity_name(entity_cls)
652
666
  logger.info("Deregistering entity class: {}", name)
@@ -658,6 +672,7 @@ class AoI:
658
672
 
659
673
  Returns:
660
674
  list[str]: A sorted list of entity name strings.
675
+
661
676
  """
662
677
  return sorted(_REGISTRY)
663
678
 
@@ -695,6 +710,7 @@ def _validate_entity_paths(entity: BaseEntity, name: str) -> None:
695
710
 
696
711
  Raises:
697
712
  EntityValidationError: If any path does not exist.
713
+
698
714
  """
699
715
  # --- Static path existence check ----------------------------------------
700
716
  try:
@@ -760,6 +776,7 @@ def _validate_entity(entity: BaseEntity, name: str) -> None:
760
776
 
761
777
  Raises:
762
778
  EntityValidationError: If any validation check fails.
779
+
763
780
  """
764
781
  # Fast path-only check first.
765
782
  _validate_entity_paths(entity, name)
@@ -65,6 +65,7 @@ def _so_key(storage_options: dict[str, Any]) -> tuple[tuple[str, Any], ...]:
65
65
 
66
66
  Returns:
67
67
  tuple[tuple[str, Any], ...]: A sorted tuple of ``(key, value)`` pairs.
68
+
68
69
  """
69
70
  return tuple(sorted(storage_options.items()))
70
71
 
@@ -85,6 +86,7 @@ def _cached_detect(
85
86
 
86
87
  Returns:
87
88
  SchemaProfile: Detected schema profile for the given entity paths.
89
+
88
90
  """
89
91
  from core_lens.schema.detection import detect
90
92
 
@@ -114,6 +116,7 @@ def _cached_build_index(
114
116
 
115
117
  Returns:
116
118
  pl.DataFrame: The bounding-box index DataFrame.
119
+
117
120
  """
118
121
  logger.debug("Building bbox index for {} (cache miss)", static_path)
119
122
  return build_bbox_index(
@@ -127,7 +130,7 @@ def _cached_build_index(
127
130
 
128
131
 
129
132
  class BaseEntity(ABC):
130
- """Abstract base class for every entity in the core_lens plugin system.
133
+ r"""Abstract base class for every entity in the core_lens plugin system.
131
134
 
132
135
  An entity represents a geospatial primitive (e.g. microwatershed, village,
133
136
  district) backed by one or more Parquet/GeoParquet files. Entities are
@@ -195,6 +198,7 @@ class BaseEntity(ABC):
195
198
  ``polars.scan_parquet``. For S3 the common keys are
196
199
  ``"region"``, ``"access_key"``, and ``"secret_key"``.
197
200
  ``None`` uses ambient credentials (env-vars / ``~/.aws/``).
201
+
198
202
  """
199
203
  # Store as str so cloud URIs (s3://…) are never coerced through pathlib.
200
204
  self._data_root: str | None = str(data_root) if data_root is not None else None
@@ -219,6 +223,7 @@ class BaseEntity(ABC):
219
223
 
220
224
  Raises:
221
225
  FileNotFoundError: For *local* paths that do not exist after resolution.
226
+
222
227
  """
223
228
  # Fully-qualified cloud URI — return as-is.
224
229
  if is_cloud_uri(path):
@@ -255,13 +260,14 @@ class BaseEntity(ABC):
255
260
  @property
256
261
  @abstractmethod
257
262
  def key_cols(self) -> list[str]:
258
- """Columns that uniquely identify one instance of this entity.
263
+ r"""Columns that uniquely identify one instance of this entity.
259
264
 
260
265
  For built-in entities this is always a single-element list (e.g.
261
266
  ``[\"mws_id\"]``), but the contract allows composite keys for plugins.
262
267
 
263
268
  Returns:
264
269
  list[str]: A list of column name strings present in the static file.
270
+
265
271
  """
266
272
 
267
273
  @property
@@ -274,6 +280,7 @@ class BaseEntity(ABC):
274
280
 
275
281
  Returns:
276
282
  str: The column name as a string.
283
+
277
284
  """
278
285
 
279
286
  @property
@@ -287,6 +294,7 @@ class BaseEntity(ABC):
287
294
 
288
295
  Returns:
289
296
  str: A path string.
297
+
290
298
  """
291
299
 
292
300
  @property
@@ -299,6 +307,7 @@ class BaseEntity(ABC):
299
307
 
300
308
  Returns:
301
309
  str | None: A path string, or ``None`` if the entity has no annual data.
310
+
302
311
  """
303
312
  return None
304
313
 
@@ -312,6 +321,7 @@ class BaseEntity(ABC):
312
321
 
313
322
  Returns:
314
323
  str | None: A path string, or ``None`` if the entity has no fortnightly data.
324
+
315
325
  """
316
326
  return None
317
327
 
@@ -336,6 +346,7 @@ class BaseEntity(ABC):
336
346
 
337
347
  Returns:
338
348
  SchemaProfile: A fully-validated :class:`~core_lens.schema.profile.SchemaProfile`.
349
+
339
350
  """
340
351
  if not hasattr(self, "_schema_profile"):
341
352
  _so = self._storage_options or {}
@@ -372,6 +383,7 @@ class BaseEntity(ABC):
372
383
 
373
384
  Returns:
374
385
  pl.DataFrame: DataFrame with columns ``(*key_cols, minx, miny, maxx, maxy)``.
386
+
375
387
  """
376
388
  if not hasattr(self, "_cached_index"):
377
389
  profile = self.schema_profile # already cached — no extra I/O
@@ -409,6 +421,7 @@ class BaseEntity(ABC):
409
421
  Raises:
410
422
  ValueError: If a kwarg cannot be resolved as either an attribute
411
423
  column or a registered entity name.
424
+
412
425
  """
413
426
  from core_lens.aoi import _REGISTRY
414
427
  from core_lens.base.view import View
@@ -544,6 +557,7 @@ class BaseEntity(ABC):
544
557
 
545
558
  Raises:
546
559
  ValueError: If neither ``geometry`` nor ``bbox`` is provided.
560
+
547
561
  """
548
562
  import shapely.geometry as sgeom
549
563
 
@@ -584,7 +598,7 @@ class BaseEntity(ABC):
584
598
  return View(keys=keys, entity=self, entity_name=entity_name)
585
599
 
586
600
  def spatial_join(self, other: "BaseEntity", agg: dict[str, str]) -> "View":
587
- """Return a lazy :class:`~core_lens.base.view.View` with a cross-entity join pending.
601
+ r"""Return a lazy :class:`~core_lens.base.view.View` with a cross-entity join pending.
588
602
 
589
603
  The join is recorded in the View's ``join_spec`` and computed only at
590
604
  materialisation time (``.static``, ``.annual``, or ``.fortnightly``).
@@ -601,6 +615,7 @@ class BaseEntity(ABC):
601
615
  Returns:
602
616
  View: A lazy :class:`~core_lens.base.view.View` with the join spec
603
617
  recorded for deferred execution.
618
+
604
619
  """
605
620
  from core_lens.base.view import View
606
621
 
@@ -17,6 +17,7 @@ class SubplotOn(Enum):
17
17
  MONTH: Split data by month.
18
18
  SEASON: Split data by meteorological season.
19
19
  SEASON_YEAR: Split data by season and year.
20
+
20
21
  """
21
22
 
22
23
  YEAR = "year"
@@ -71,6 +72,7 @@ def _wkb_to_arrow_table(
71
72
 
72
73
  Returns:
73
74
  An ``arro3.core.Table`` whose geometry column is GeoArrow-typed.
75
+
74
76
  """
75
77
  import geoarrow.rust.core as ga
76
78
  import polars as pl
@@ -182,6 +184,7 @@ def _color_for(index: int) -> str:
182
184
 
183
185
  Returns:
184
186
  The hex color string.
187
+
185
188
  """
186
189
  return _PALETTE[index % len(_PALETTE)]
187
190
 
@@ -193,6 +196,7 @@ def _apply_theme(fig: Any, result: "Result", title: str) -> None:
193
196
  fig: The Bokeh figure.
194
197
  result: The parent Result object.
195
198
  title: The title text.
199
+
196
200
  """
197
201
  entity_name = getattr(result.entity, "__name__", "Unknown")
198
202
  if hasattr(result, "entity_name"):
@@ -202,7 +206,6 @@ def _apply_theme(fig: Any, result: "Result", title: str) -> None:
202
206
  fig.title.text_font = "Inter, sans-serif"
203
207
  fig.title.text_font_size = "14pt"
204
208
 
205
- # Subtitle-style annotation in the bottom-right corner.
206
209
  from bokeh.models.annotations import Label
207
210
 
208
211
  subtitle = Label(
@@ -227,6 +230,7 @@ class PlotNamespace:
227
230
 
228
231
  Attributes:
229
232
  result: The parent Result object.
233
+
230
234
  """
231
235
 
232
236
  def __init__(self, result: "Result") -> None:
@@ -253,6 +257,7 @@ class PlotNamespace:
253
257
  ValueError: If ``column`` or ``subplot_on`` column not found in data.
254
258
  NotImplementedError: Lonboard does not support native subplot grids;
255
259
  only single-value ``subplot_on`` is rendered when specified.
260
+
256
261
  """
257
262
  import lonboard
258
263
  from lonboard.colormap import apply_continuous_cmap
@@ -347,6 +352,7 @@ class PlotNamespace:
347
352
 
348
353
  Returns:
349
354
  Any: A Bokeh Tabs object (or a single Figure when ``aggregate=True``).
355
+
350
356
  """
351
357
  import polars as pl
352
358
  import polars.selectors as cs
@@ -474,7 +480,6 @@ class PlotNamespace:
474
480
  _apply_theme(fig, self.result, title)
475
481
  return fig
476
482
 
477
- # --- Aggregated view --------------------------------------------------
478
483
  group_cols = [x, subplot_col] if subplot_col else [x]
479
484
  agg_pdf = (
480
485
  df.group_by(group_cols)
@@ -486,7 +491,6 @@ class PlotNamespace:
486
491
  if aggregate:
487
492
  return _make_fig(agg_pdf, "Timeseries (Aggregated Mean)")
488
493
 
489
- # --- Per-entity view --------------------------------------------------
490
494
  entity_df = df.sort(x)
491
495
  unique_entities = entity_df[key_col].unique()
492
496
  if len(unique_entities) > top_n:
@@ -519,6 +523,7 @@ class PlotNamespace:
519
523
 
520
524
  Returns:
521
525
  Any: A Bokeh Figure or Tabs object.
526
+
522
527
  """
523
528
  import polars as pl
524
529
  import polars.selectors as cs
@@ -595,6 +600,7 @@ class PlotNamespace:
595
600
 
596
601
  Returns:
597
602
  Any: A Bokeh Figure or Tabs object.
603
+
598
604
  """
599
605
  import numpy as np
600
606
  import pandas as pd
@@ -665,6 +671,7 @@ class PlotNamespace:
665
671
 
666
672
  Returns:
667
673
  Any: A Bokeh Figure object.
674
+
668
675
  """
669
676
  import polars.selectors as cs
670
677
  from bokeh.models import BasicTicker, ColumnDataSource, LinearColorMapper
@@ -684,7 +691,6 @@ class PlotNamespace:
684
691
  corr = df.to_pandas().corr()
685
692
  cols = list(corr.columns)
686
693
 
687
- # Build long-form data for rect glyphs.
688
694
  xs: list[str] = []
689
695
  ys: list[str] = []
690
696
  vals: list[float] = []
@@ -744,6 +750,7 @@ class PlotNamespace:
744
750
 
745
751
  Returns:
746
752
  Any: A Bokeh Figure object.
753
+
747
754
  """
748
755
  from bokeh.models import (
749
756
  BasicTicker,
@@ -769,7 +776,6 @@ class PlotNamespace:
769
776
  x_vals = [str(v) for v in pdf.columns.tolist()]
770
777
  y_vals = [str(v) for v in pdf.index.tolist()]
771
778
 
772
- # Long-form
773
779
  xs2: list[str] = []
774
780
  ys2: list[str] = []
775
781
  vals2: list[float] = []
@@ -819,6 +825,7 @@ class PlotNamespace:
819
825
 
820
826
  Returns:
821
827
  Any: A Bokeh ``gridplot`` object.
828
+
822
829
  """
823
830
  import polars.selectors as cs
824
831
  from bokeh.layouts import gridplot
@@ -29,6 +29,7 @@ def _sf(x: object) -> float:
29
29
 
30
30
  Returns:
31
31
  float: The numeric value as a float, or NaN if the input is None.
32
+
32
33
  """
33
34
  if x is None:
34
35
  return float("nan")
@@ -42,6 +43,7 @@ class CorrelateMethod(Enum):
42
43
  PEARSON: Pearson correlation coefficient.
43
44
  SPEARMAN: Spearman rank correlation.
44
45
  KENDALL: Kendall Tau correlation.
46
+
45
47
  """
46
48
 
47
49
  PEARSON = "pearson"
@@ -58,6 +60,7 @@ class TestMethod(Enum):
58
60
  WILCOXON: Wilcoxon signed-rank test.
59
61
  KS: Kolmogorov-Smirnov test.
60
62
  CHI_SQUARE: Chi-square test.
63
+
61
64
  """
62
65
 
63
66
  __test__ = False
@@ -75,6 +78,7 @@ class ChangeMethod(Enum):
75
78
  ABSOLUTE: Absolute difference between periods.
76
79
  PERCENTAGE: Percentage change between periods.
77
80
  TREND: Linear trend over time.
81
+
78
82
  """
79
83
 
80
84
  ABSOLUTE = "absolute"
@@ -90,6 +94,7 @@ class AnomalyCrossMethod(Enum):
90
94
  IQR: Interquartile range method.
91
95
  PERCENTILE: Percentile-based method.
92
96
  THRESHOLD: Fixed threshold method.
97
+
93
98
  """
94
99
 
95
100
  ZSCORE = "zscore"
@@ -105,6 +110,7 @@ class AnomalyTsMethod(Enum):
105
110
  STL: Seasonal-Trend decomposition using LOESS.
106
111
  CUSUM: Cumulative sum control chart.
107
112
  MAD: Median Absolute Deviation.
113
+
108
114
  """
109
115
 
110
116
  STL = "stl"
@@ -120,6 +126,7 @@ class SimilarityMethod(Enum):
120
126
  COSINE: Cosine similarity.
121
127
  MAHALANOBIS: Mahalanobis distance.
122
128
  MANHATTAN: Manhattan distance.
129
+
123
130
  """
124
131
 
125
132
  EUCLIDEAN = "euclidean"
@@ -167,7 +174,8 @@ class StatsNamespace:
167
174
  columns: list[str] | None = None,
168
175
  by: str = "column",
169
176
  ) -> "Result":
170
- """Per-column or per-entity descriptive statistics.
177
+ r"""Per-column or per-entity descriptive statistics.
178
+
171
179
  Uses polars' in-built methods for mean, std, min, max, quantiles etc.
172
180
 
173
181
  Args:
@@ -180,6 +188,7 @@ class StatsNamespace:
180
188
 
181
189
  Raises:
182
190
  ValueError: If ``by`` is not ``\"column\"`` or ``\"entity\"``.
191
+
183
192
  """
184
193
  df = self._r.data
185
194
  cols = columns if columns is not None else self._numeric_cols(df)
@@ -239,6 +248,7 @@ class StatsNamespace:
239
248
  Raises:
240
249
  CorrelationError: If fewer than 2 columns supplied.
241
250
  ValueError: If ``method`` is not recognised.
251
+
242
252
  """
243
253
  if len(columns) < 2:
244
254
  raise CorrelationError(
@@ -313,6 +323,7 @@ class StatsNamespace:
313
323
  Raises:
314
324
  ValueError: If none of ``groups``, ``periods``, ``against`` supplied,
315
325
  or if ``method`` is not recognised.
326
+
316
327
  """
317
328
  import scipy.stats as sp
318
329
 
@@ -460,6 +471,7 @@ class StatsNamespace:
460
471
 
461
472
  Raises:
462
473
  ValueError: If ``method`` is not recognised or year column absent.
474
+
463
475
  """
464
476
  if not isinstance(method, ChangeMethod):
465
477
  raise ValueError(
@@ -566,6 +578,7 @@ class StatsNamespace:
566
578
 
567
579
  Raises:
568
580
  ValueError: If ``mode``, ``method``, or observation count invalid.
581
+
569
582
  """
570
583
  df = self._r.data
571
584
  key = self._r.key_cols[0]
@@ -835,6 +848,7 @@ class StatsNamespace:
835
848
  Raises:
836
849
  ValueError: If ``method`` is invalid, no columns can be resolved,
837
850
  or ``target`` is not found.
851
+
838
852
  """
839
853
  if not isinstance(method, SimilarityMethod):
840
854
  raise ValueError(